gitscope
← rgit / main / src/methods/repo/snapshot.rs
https://dev.kronoz.win/r/rgit.git
src/methods/repo/snapshot.rs · Rust
Blame Raw
 1use std::sync::Arc;
 2
 3use anyhow::{Context, anyhow};
 4use axum::{Extension, body::Body, extract::Query, http::Response};
 5use serde::Deserialize;
 6use tokio_stream::wrappers::ReceiverStream;
 7use tracing::{Instrument, error, info_span};
 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}