1use std::sync::Arc;
2
3use anyhow::{anyhow, Context};
4use axum::{body::Body, extract::Query, http::Response, Extension};
5use serde::Deserialize;
6use tokio_stream::wrappers::ReceiverStream;
7use tracing::{error, info_span, Instrument};
8
9use super::{RepositoryPath, Result};
10use crate::git::Git;
11
12#[derive(Deserialize)]
13pub struct UriQuery {
14 #[serde(rename = "h")]
15 branch: Option<Arc<str>>,
16 id: Option<Arc<str>>,
17}
18
19pub async fn handle(
20 Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
21 Extension(git): Extension<Arc<Git>>,
22 Query(query): Query<UriQuery>,
23) -> Result<Response<Body>> {
24 let open_repo = git.repo(repository_path, query.branch.clone()).await?;
25
26 // byte stream back to the client
27 let (send, recv) = tokio::sync::mpsc::channel(1);
28
29 // channel for `archive` to tell us we can send headers etc back to
30 // the user so it has time to return an error
31 let (send_cont, recv_cont) = tokio::sync::oneshot::channel();
32
33 let id = query.id.clone();
34
35 let res = tokio::spawn(
36 async move {
37 if let Err(error) = open_repo
38 .archive(send.clone(), send_cont, id.as_deref())
39 .await
40 {
41 error!(%error, "Failed to build archive for client");
42 let _res = send.send(Err(anyhow!("archive builder failed"))).await;
43 return Err(error);
44 }
45
46 Ok(())
47 }
48 .instrument(info_span!("sender")),
49 );
50
51 // don't send any headers until `archive` has told us we're good
52 // to continue
53 if recv_cont.await.is_err() {
54 // sender disappearing means `archive` hit an issue during init, lets
55 // wait for the error back from the spawned tokio task to return to
56 // the client
57 res.await
58 .context("Tokio task failed")?
59 .context("Failed to build archive")?;
60
61 // ok, well this isn't ideal. the sender disappeared but we never got
62 // an error. this shouldn't be possible, i guess lets just return an
63 // internal error
64 return Err(anyhow!("Ran into inconsistent error state whilst building archive, please file an issue at https://github.com/w4/rgit/issues").into());
65 }
66
67 let file_name = query
68 .id
69 .as_deref()
70 .or(query.branch.as_deref())
71 .unwrap_or("main");
72
73 Ok(Response::builder()
74 .header("Content-Type", "application/gzip")
75 .header(
76 "Content-Disposition",
77 format!("attachment; filename=\"{file_name}.tar.gz\""),
78 )
79 .body(Body::from_stream(ReceiverStream::new(recv)))
80 .context("failed to build response")?)
81}