Files
gitks/archive/get_archive.rs
T
zhenyi d243dce027 refactor(server): replace custom remote clients with macro-based implementation
- Replaced manual remote client functions with remote_client! macro for archive, blame, branch, commit, and diff services
- Simplified remote client creation logic using declarative macro approach
- Maintained same functionality while reducing code duplication across services

security(bare): enhance path traversal protection with comprehensive validation

- Added early relative_path validation to prevent path traversal attacks
- Implemented unified path validation to avoid TOCTOU race conditions
- Enhanced canonicalization checks for both existing and non-existent paths
- Added detailed logging for path traversal detection attempts

feat(cache): migrate from CLruCache to Moka with TTL and invalidation support

- Replaced clru dependency with moka for improved caching capabilities
- Added 300-second time-to-live for cache entries
- Implemented repository-specific cache invalidation mechanism
- Enhanced cache operations with thread-safe async support

refactor(commit): improve security validation for commit operations

- Added ref name validation to prevent command injection in cherry_pick_commit
- Implemented revision validation for commit selectors
- Added comprehensive input validation for create_commit parameters
- Enhanced file path validation to prevent traversal
2026-06-08 09:43:57 +08:00

120 lines
4.3 KiB
Rust

use std::process::{Command, Stdio};
use tokio_stream::wrappers::ReceiverStream;
use crate::bare::GitBare;
use crate::pb::{ArchiveChunk, ArchiveRequest, archive_options, object_selector};
impl GitBare {
/// Stream archive data via a channel to avoid loading the entire archive into memory.
/// Returns a ReceiverStream that yields ArchiveChunk as the subprocess produces output.
pub fn get_archive_stream(
&self,
request: ArchiveRequest,
) -> Result<ReceiverStream<Result<ArchiveChunk, tonic::Status>>, tonic::Status> {
let bare_dir = self.bare_dir.clone();
tracing::info!(
repo = %bare_dir.display(),
"spawning git archive subprocess"
);
let (tx, rx) = tokio::sync::mpsc::channel(16);
// Validate revision before spawning (cannot use ? inside spawn_blocking closure)
let revision = match request.treeish.and_then(|s| s.selector) {
Some(object_selector::Selector::Oid(oid)) => oid.hex,
Some(object_selector::Selector::Revision(name)) => {
crate::sanitize::validate_revision(&name.revision)
.map_err(|e| tonic::Status::invalid_argument(e.to_string()))?;
name.revision
}
None => "HEAD".into(),
};
// Spawn the blocking git subprocess in a dedicated thread
tokio::task::spawn_blocking(move || {
let options = request.options.unwrap_or_default();
let format = archive_options::Format::try_from(options.format)
.unwrap_or(archive_options::Format::ArchiveFormatTar);
let mut args = vec!["archive".to_string()];
args.push(match format {
archive_options::Format::ArchiveFormatZip => "--format=zip".into(),
_ => "--format=tar".into(),
});
if !options.prefix.is_empty() {
args.push(format!("--prefix={}", options.prefix));
}
args.push(revision);
if !options.pathspec.is_empty() {
args.push("--".into());
args.extend(options.pathspec);
}
let mut child = match Command::new("git")
.arg("--git-dir")
.arg(&bare_dir)
.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(c) => c,
Err(e) => {
let _ = tx.blocking_send(Err(tonic::Status::internal(format!(
"failed to spawn git archive: {e}"
))));
return;
}
};
let stdout = match child.stdout.take() {
Some(s) => s,
None => {
let _ =
tx.blocking_send(Err(tonic::Status::internal("failed to capture stdout")));
return;
}
};
// Read stdout in 64KB chunks and stream them
use std::io::Read;
let mut reader = std::io::BufReader::new(stdout);
let mut buf = vec![0u8; 65536];
loop {
match reader.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let chunk = ArchiveChunk {
data: buf[..n].to_vec(),
};
if tx.blocking_send(Ok(chunk)).is_err() {
break;
}
}
Err(e) => {
let _ = tx.blocking_send(Err(tonic::Status::internal(format!(
"read error: {e}"
))));
break;
}
}
}
match child.wait() {
Ok(status) if !status.success() => {
let _ = tx.blocking_send(Err(tonic::Status::internal(
"git archive exited with error",
)));
}
Err(e) => {
let _ =
tx.blocking_send(Err(tonic::Status::internal(format!("wait error: {e}"))));
}
_ => {}
}
});
Ok(ReceiverStream::new(rx))
}
}