1
8use std::io::Read;
9use std::process::{Command, Stdio};
10
11use axum::extract::{Path, State};
12use axum::http::header;
13use axum::http::HeaderValue;
14use axum::response::IntoResponse;
15use serde::Deserialize;
16use tokio::task::spawn_blocking;
17
18use crate::error::{Error, Result};
19use crate::routes::{AppState, RepoName};
20
21#[derive(Deserialize)]
23pub(super) struct ArchivePath {
24 format: String,
25 #[serde(rename = "ref")]
26 ref_: String,
27}
28
29struct ArchiveConfig {
30 content_type: &'static str,
31 extension: &'static str,
32}
33
34fn archive_config(format: &str) -> Option<ArchiveConfig> {
37 match format {
38 "tar.gz" | "tgz" => Some(ArchiveConfig {
39 content_type: "application/gzip",
40 extension: "tar.gz",
41 }),
42 "tar.xz" | "txz" => Some(ArchiveConfig {
43 content_type: "application/x-xz",
44 extension: "tar.xz",
45 }),
46 "zip" => Some(ArchiveConfig {
47 content_type: "application/zip",
48 extension: "zip",
49 }),
50 _ => None,
51 }
52}
53
54fn run_archive(repo_path: &str, git_format: &str, ref_: &str) -> (Vec<u8>, bool, String) {
56 let output = Command::new("git")
57 .arg("-C")
58 .arg(repo_path)
59 .arg("archive")
60 .arg(format!("--format={git_format}"))
61 .arg(ref_)
62 .output();
63
64 match output {
65 Ok(o) => (
66 o.stdout,
67 o.status.success(),
68 String::from_utf8_lossy(&o.stderr).into_owned(),
69 ),
70 Err(e) => (Vec::new(), false, e.to_string()),
71 }
72}
73
74fn run_archive_xz(repo_path: &str, ref_: &str) -> (Vec<u8>, bool, String) {
76 let mut git = match Command::new("git")
77 .arg("-C")
78 .arg(repo_path)
79 .arg("archive")
80 .arg("--format=tar")
81 .arg(ref_)
82 .stdout(Stdio::piped())
83 .stderr(Stdio::piped())
84 .spawn()
85 {
86 Ok(c) => c,
87 Err(e) => return (Vec::new(), false, format!("spawn git: {e}")),
88 };
89
90 let git_stdout = git.stdout.take().expect("stdout configured");
91 let mut xz = match Command::new("xz")
92 .stdin(git_stdout)
93 .stdout(Stdio::piped())
94 .stderr(Stdio::piped())
95 .spawn()
96 {
97 Ok(c) => c,
98 Err(e) => return (Vec::new(), false, format!("spawn xz: {e}")),
99 };
100
101 let xz_output = match xz.wait_with_output() {
103 Ok(o) => o,
104 Err(e) => return (Vec::new(), false, format!("xz wait: {e}")),
105 };
106
107 let git_status = match git.wait() {
110 Ok(s) => s,
111 Err(e) => return (Vec::new(), false, format!("git wait: {e}")),
112 };
113
114 if !git_status.success() {
115 let msg = git.stderr.take()
116 .and_then(|mut s| {
117 let mut buf = String::new();
118 s.read_to_string(&mut buf).ok()?;
119 Some(buf)
120 })
121 .unwrap_or_default();
122 let msg = if msg.is_empty() { "git archive failed".into() } else { msg };
123 return (Vec::new(), false, msg);
124 }
125
126 if !xz_output.status.success() {
127 let msg = String::from_utf8_lossy(&xz_output.stderr);
128 let msg = if msg.is_empty() { "xz compression failed".into() } else { msg.into_owned() };
129 return (Vec::new(), false, msg);
130 }
131
132 (xz_output.stdout, true, String::new())
133}
134
135pub async fn handler(
137 State(state): State<AppState>,
138 RepoName(name): RepoName,
139 Path(params): Path<ArchivePath>,
140) -> Result<impl IntoResponse> {
141 if params.ref_.is_empty() {
142 return Err(Error::BadRequest("empty ref".into()));
143 }
144
145 let cfg = archive_config(¶ms.format)
146 .ok_or_else(|| Error::BadRequest(format!("unsupported format: {}", params.format)))?;
147
148 let repo_path = state.root.join(&name);
149 if !repo_path.exists() {
150 return Err(Error::RepoNotFound(name.clone()));
151 }
152
153 let git_repo = gix::open(&repo_path).map_err(|_| Error::RepoNotFound(name.clone()))?;
155 if git_repo.rev_parse_single(params.ref_.as_str()).is_err() {
156 return Err(Error::NotFound(format!("ref not found: {}", params.ref_)));
157 }
158
159 let repo_path_str = repo_path.to_string_lossy().into_owned();
160 let archive_ref = params.ref_.clone();
161
162 let uses_xz = matches!(params.format.as_str(), "tar.xz" | "txz");
163 let git_format = match params.format.as_str() {
164 "zip" => "zip",
165 _ => "tar.gz",
166 };
167 let ref_for_archive = archive_ref.clone();
168
169 let (stdout, success, stderr) = spawn_blocking(move || {
170 if uses_xz {
171 run_archive_xz(&repo_path_str, &ref_for_archive)
172 } else {
173 run_archive(&repo_path_str, git_format, &ref_for_archive)
174 }
175 })
176 .await
177 .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
178
179 if !success {
180 return Err(Error::NotFound(format!("archive failed: {stderr}")));
181 }
182
183 let short_ref = archive_ref
185 .strip_prefix("refs/heads/")
186 .or_else(|| archive_ref.strip_prefix("refs/tags/"))
187 .unwrap_or(&archive_ref);
188 let filename = format!("{name}-{short_ref}.{}", cfg.extension);
189 let disposition = HeaderValue::from_str(&format!("attachment; filename=\"{filename}\""))
190 .map_err(|_| Error::BadRequest("invalid header value".into()))?;
191
192 let content_type = HeaderValue::from_static(cfg.content_type);
193
194 Ok((
195 [
196 (header::CONTENT_TYPE, content_type),
197 (header::CONTENT_DISPOSITION, disposition),
198 ],
199 stdout,
200 ))
201}