mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-04 03:00:32 +08:00
httpd: Add tar.zst archive downloads
Support /raw/<rid>/archive/<ref>.tar.zst next to the existing tar, tar.gz and zip formats. Git has no builtin zstd tar format, so register one via tar.tar.zst.command. Needs the zstd binary in PATH.
This commit is contained in:
@@ -11,6 +11,7 @@ Homepage: https://radicle.dev/
|
||||
Package: radicle-httpd
|
||||
Architecture: any
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}, git
|
||||
Recommends: zstd
|
||||
Description: HTTP API daemon for Radicle peer-to-peer code collaboration
|
||||
This package provides a standalone HTTP API service for interacting with
|
||||
Radicle's distributed storage and peer-to-peer network. It enables external
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Query, State};
|
||||
@@ -27,10 +27,21 @@ use crate::error::RawError as Error;
|
||||
|
||||
const MAX_BLOB_SIZE: usize = 10_485_760;
|
||||
|
||||
/// Whether the `zstd` binary that `tar.zst` archives shell out to is in `PATH`.
|
||||
static ZSTD_AVAILABLE: LazyLock<bool> = LazyLock::new(|| {
|
||||
std::process::Command::new("zstd")
|
||||
.arg("--version")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.is_ok_and(|status| status.success())
|
||||
});
|
||||
|
||||
/// Values for `git archive --format` that we support.
|
||||
#[derive(Debug, Default)]
|
||||
enum ArchiveFormat {
|
||||
Tar,
|
||||
TarZst,
|
||||
// NOTE: Even though `git archive` would use `tar` as the default format,
|
||||
// we use `tar.gz` as the default format for HTTP responses, to benefit
|
||||
// from compression when downloading large repositories. It was also used
|
||||
@@ -46,6 +57,7 @@ impl ArchiveFormat {
|
||||
match self {
|
||||
ArchiveFormat::Tar => "tar",
|
||||
ArchiveFormat::TarGz => "tar.gz",
|
||||
ArchiveFormat::TarZst => "tar.zst",
|
||||
ArchiveFormat::Zip => "zip",
|
||||
}
|
||||
}
|
||||
@@ -54,6 +66,7 @@ impl ArchiveFormat {
|
||||
match self {
|
||||
ArchiveFormat::Tar => ".tar",
|
||||
ArchiveFormat::TarGz => ".tar.gz",
|
||||
ArchiveFormat::TarZst => ".tar.zst",
|
||||
ArchiveFormat::Zip => ".zip",
|
||||
}
|
||||
}
|
||||
@@ -64,6 +77,7 @@ impl ArchiveFormat {
|
||||
match self {
|
||||
ArchiveFormat::Tar => "application/x-tar",
|
||||
ArchiveFormat::TarGz => "application/gzip",
|
||||
ArchiveFormat::TarZst => "application/zstd",
|
||||
ArchiveFormat::Zip => "application/zip",
|
||||
}
|
||||
}
|
||||
@@ -78,6 +92,8 @@ impl ArchiveFormat {
|
||||
(stripped, Some(ArchiveFormat::Tar))
|
||||
} else if let Some(stripped) = s.strip_suffix(ArchiveFormat::TarGz.extension()) {
|
||||
(stripped, Some(ArchiveFormat::TarGz))
|
||||
} else if let Some(stripped) = s.strip_suffix(ArchiveFormat::TarZst.extension()) {
|
||||
(stripped, Some(ArchiveFormat::TarZst))
|
||||
} else if let Some(stripped) = s.strip_suffix(ArchiveFormat::Zip.extension()) {
|
||||
(stripped, Some(ArchiveFormat::Zip))
|
||||
} else {
|
||||
@@ -196,6 +212,11 @@ async fn archive_by_committish(
|
||||
format: ArchiveFormat,
|
||||
profile: Arc<Profile>,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
// Without zstd, git would stream an empty body after a 200 response.
|
||||
if matches!(format, ArchiveFormat::TarZst) && !*ZSTD_AVAILABLE {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
let storage = &profile.storage;
|
||||
let repo = storage.repository(rid)?;
|
||||
|
||||
@@ -265,6 +286,11 @@ async fn archive_by_committish(
|
||||
|
||||
let mut command = Command::new("git");
|
||||
|
||||
// Git has no builtin zstd tar format, so we register one.
|
||||
if matches!(format, ArchiveFormat::TarZst) {
|
||||
command.arg("-c").arg("tar.tar.zst.command=zstd");
|
||||
}
|
||||
|
||||
command
|
||||
.arg("archive")
|
||||
.arg(format!("--format={}", format.as_str()));
|
||||
@@ -392,6 +418,34 @@ mod routes {
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_archive_tar_zst() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test::seed(tmp.path());
|
||||
let app = super::router(ctx.profile().to_owned(), Arc::new(HashMap::new()));
|
||||
|
||||
let response = get(&app, format!("/{RID}/archive/master.tar.zst")).await;
|
||||
|
||||
// Git shells out to zstd for this format.
|
||||
if !*super::ZSTD_AVAILABLE {
|
||||
assert!(
|
||||
std::env::var_os("CI").is_none(),
|
||||
"zstd must be in PATH on CI"
|
||||
);
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.headers().get("Content-Type").unwrap(),
|
||||
"application/zstd"
|
||||
);
|
||||
|
||||
// zstd magic number.
|
||||
let body = response.body().await;
|
||||
assert_eq!(&body[..4], &[0x28, 0xb5, 0x2f, 0xfd]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_alias_resolution() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user