~//! Archive download handler.~//!~//! Covers the route `/{repo}/archive/{format}/{*ref}`. Streams the-//! repository archive in the requested format (`tar.gz`, `tar.xz`, or-//! `zip`) for the given ref.+//! repository archive in the requested format (`tar.gz`, `tgz`, `tar.xz`,+//! `txz`, or `zip`) for the given ref. `tar.xz` and `txz` are produced by+//! piping `git archive --format=tar` through an external `xz` process.~+use std::io::Read;+use std::process::{Command, Stdio};++use axum::extract::{Path, State};+use axum::http::header;+use axum::http::HeaderValue;~use axum::response::IntoResponse;+use serde::Deserialize;+use tokio::task::spawn_blocking;++use crate::error::{Error, Result};+use crate::routes::{AppState, RepoName};++/// Path parameters for the archive route.+#[derive(Deserialize)]+pub(super) struct ArchivePath {+ format: String,+ #[serde(rename = "ref")]+ ref_: String,+}++struct ArchiveConfig {+ content_type: &'static str,+ extension: &'static str,+}++/// Returns the content type and file extension for a given format string.+/// Returns `None` for unsupported formats.+fn archive_config(format: &str) -> Option<ArchiveConfig> {+ match format {+ "tar.gz" | "tgz" => Some(ArchiveConfig {+ content_type: "application/gzip",+ extension: "tar.gz",+ }),+ "tar.xz" | "txz" => Some(ArchiveConfig {+ content_type: "application/x-xz",+ extension: "tar.xz",+ }),+ "zip" => Some(ArchiveConfig {+ content_type: "application/zip",+ extension: "zip",+ }),+ _ => None,+ }+}++/// Runs `git archive` and returns (stdout, success, stderr).+fn run_archive(repo_path: &str, git_format: &str, ref_: &str) -> (Vec<u8>, bool, String) {+ let output = Command::new("git")+ .arg("-C")+ .arg(repo_path)+ .arg("archive")+ .arg(format!("--format={git_format}"))+ .arg(ref_)+ .output();++ match output {+ Ok(o) => (+ o.stdout,+ o.status.success(),+ String::from_utf8_lossy(&o.stderr).into_owned(),+ ),+ Err(e) => (Vec::new(), false, e.to_string()),+ }+}++/// Pipes `git archive --format=tar` through `xz`, returning (stdout, success, stderr).+fn run_archive_xz(repo_path: &str, ref_: &str) -> (Vec<u8>, bool, String) {+ let mut git = match Command::new("git")+ .arg("-C")+ .arg(repo_path)+ .arg("archive")+ .arg("--format=tar")+ .arg(ref_)+ .stdout(Stdio::piped())+ .stderr(Stdio::piped())+ .spawn()+ {+ Ok(c) => c,+ Err(e) => return (Vec::new(), false, format!("spawn git: {e}")),+ };++ let git_stdout = git.stdout.take().expect("stdout configured");+ let mut xz = match Command::new("xz")+ .stdin(git_stdout)+ .stdout(Stdio::piped())+ .stderr(Stdio::piped())+ .spawn()+ {+ Ok(c) => c,+ Err(e) => return (Vec::new(), false, format!("spawn xz: {e}")),+ };++ // Wait for xz to finish (consumes all piped git output).+ let xz_output = match xz.wait_with_output() {+ Ok(o) => o,+ Err(e) => return (Vec::new(), false, format!("xz wait: {e}")),+ };++ // Check git's exit status. Report git errors first since xz failures+ // are often a downstream consequence of git producing no output.+ let git_status = match git.wait() {+ Ok(s) => s,+ Err(e) => return (Vec::new(), false, format!("git wait: {e}")),+ };++ if !git_status.success() {+ let msg = git.stderr.take()+ .and_then(|mut s| {+ let mut buf = String::new();+ s.read_to_string(&mut buf).ok()?;+ Some(buf)+ })+ .unwrap_or_default();+ let msg = if msg.is_empty() { "git archive failed".into() } else { msg };+ return (Vec::new(), false, msg);+ }++ if !xz_output.status.success() {+ let msg = String::from_utf8_lossy(&xz_output.stderr);+ let msg = if msg.is_empty() { "xz compression failed".into() } else { msg.into_owned() };+ return (Vec::new(), false, msg);+ }++ (xz_output.stdout, true, String::new())+}++/// Streams a repository archive for the given ref in the requested format.+pub async fn handler(+ State(state): State<AppState>,+ RepoName(name): RepoName,+ Path(params): Path<ArchivePath>,+) -> Result<impl IntoResponse> {+ if params.ref_.is_empty() {+ return Err(Error::BadRequest("empty ref".into()));+ }++ let cfg = archive_config(¶ms.format)+ .ok_or_else(|| Error::BadRequest(format!("unsupported format: {}", params.format)))?;++ let repo_path = state.root.join(&name);+ if !repo_path.exists() {+ return Err(Error::RepoNotFound(name.clone()));+ }++ // Verify the ref resolves before shelling out.+ let git_repo = gix::open(&repo_path).map_err(|_| Error::RepoNotFound(name.clone()))?;+ if git_repo.rev_parse_single(params.ref_.as_str()).is_err() {+ return Err(Error::NotFound(format!("ref not found: {}", params.ref_)));+ }++ let repo_path_str = repo_path.to_string_lossy().into_owned();+ let archive_ref = params.ref_.clone();++ let uses_xz = matches!(params.format.as_str(), "tar.xz" | "txz");+ let git_format = match params.format.as_str() {+ "zip" => "zip",+ _ => "tar.gz",+ };+ let ref_for_archive = archive_ref.clone();++ let (stdout, success, stderr) = spawn_blocking(move || {+ if uses_xz {+ run_archive_xz(&repo_path_str, &ref_for_archive)+ } else {+ run_archive(&repo_path_str, git_format, &ref_for_archive)+ }+ })+ .await+ .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;++ if !success {+ return Err(Error::NotFound(format!("archive failed: {stderr}")));+ }++ // Strip refs/heads/ or refs/tags/ prefix for a cleaner filename.+ let short_ref = archive_ref+ .strip_prefix("refs/heads/")+ .or_else(|| archive_ref.strip_prefix("refs/tags/"))+ .unwrap_or(&archive_ref);+ let filename = format!("{name}-{short_ref}.{}", cfg.extension);+ let disposition = HeaderValue::from_str(&format!("attachment; filename=\"{filename}\""))+ .map_err(|_| Error::BadRequest("invalid header value".into()))?;++ let content_type = HeaderValue::from_static(cfg.content_type);~-pub async fn handler() -> impl IntoResponse {- "Archive handler"+ Ok((+ [+ (header::CONTENT_TYPE, content_type),+ (header::CONTENT_DISPOSITION, disposition),+ ],+ stdout,+ ))~}