119 lines
3.8 KiB
Rust
119 lines
3.8 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum RsyncFetchError {
|
|
#[error("rsync fetch error: {0}")]
|
|
Fetch(String),
|
|
}
|
|
|
|
pub type RsyncFetchResult<T> = Result<T, RsyncFetchError>;
|
|
|
|
/// Fetch repository objects from a publication point.
|
|
///
|
|
/// v1: this is intentionally abstract so unit tests can use a mock, and later we can
|
|
/// back it by calling the system `rsync` binary (RFC 6481 §5; RFC 8182 §3.4.5).
|
|
pub trait RsyncFetcher {
|
|
/// Return a list of objects as `(rsync_uri, bytes)` pairs.
|
|
fn fetch_objects(&self, rsync_base_uri: &str) -> RsyncFetchResult<Vec<(String, Vec<u8>)>>;
|
|
}
|
|
|
|
/// A simple "rsync" implementation backed by a local directory.
|
|
///
|
|
/// This is primarily meant for offline tests and fixtures. The key generation mimics rsync URIs:
|
|
/// `rsync_base_uri` + relative path (with `/` separators).
|
|
pub struct LocalDirRsyncFetcher {
|
|
pub root_dir: PathBuf,
|
|
}
|
|
|
|
impl LocalDirRsyncFetcher {
|
|
pub fn new(root_dir: impl Into<PathBuf>) -> Self {
|
|
Self {
|
|
root_dir: root_dir.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RsyncFetcher for LocalDirRsyncFetcher {
|
|
fn fetch_objects(&self, rsync_base_uri: &str) -> RsyncFetchResult<Vec<(String, Vec<u8>)>> {
|
|
let base = normalize_rsync_base_uri(rsync_base_uri);
|
|
let mut out = Vec::new();
|
|
walk_dir_collect(&self.root_dir, &self.root_dir, &base, &mut out)
|
|
.map_err(|e| RsyncFetchError::Fetch(e))?;
|
|
Ok(out)
|
|
}
|
|
}
|
|
|
|
fn walk_dir_collect(
|
|
root: &Path,
|
|
current: &Path,
|
|
rsync_base_uri: &str,
|
|
out: &mut Vec<(String, Vec<u8>)>,
|
|
) -> Result<(), String> {
|
|
let rd = std::fs::read_dir(current).map_err(|e| e.to_string())?;
|
|
for entry in rd {
|
|
let entry = entry.map_err(|e| e.to_string())?;
|
|
let path = entry.path();
|
|
let meta = entry.metadata().map_err(|e| e.to_string())?;
|
|
if meta.is_dir() {
|
|
walk_dir_collect(root, &path, rsync_base_uri, out)?;
|
|
continue;
|
|
}
|
|
if !meta.is_file() {
|
|
continue;
|
|
}
|
|
let rel = path
|
|
.strip_prefix(root)
|
|
.map_err(|e| e.to_string())?
|
|
.to_string_lossy()
|
|
.replace('\\', "/");
|
|
let uri = format!("{rsync_base_uri}{rel}");
|
|
let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
|
|
out.push((uri, bytes));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn normalize_rsync_base_uri(s: &str) -> String {
|
|
if s.ends_with('/') {
|
|
s.to_string()
|
|
} else {
|
|
format!("{s}/")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn local_dir_rsync_fetcher_collects_files_and_normalizes_base_uri() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
std::fs::create_dir_all(tmp.path().join("nested")).expect("mkdir");
|
|
std::fs::write(tmp.path().join("a.mft"), b"a").expect("write");
|
|
std::fs::write(tmp.path().join("nested").join("b.roa"), b"b").expect("write");
|
|
|
|
let f = LocalDirRsyncFetcher::new(tmp.path());
|
|
let mut objects = f
|
|
.fetch_objects("rsync://example.net/repo")
|
|
.expect("fetch_objects");
|
|
objects.sort_by(|(a, _), (b, _)| a.cmp(b));
|
|
|
|
assert_eq!(objects.len(), 2);
|
|
assert_eq!(objects[0].0, "rsync://example.net/repo/a.mft");
|
|
assert_eq!(objects[0].1, b"a");
|
|
assert_eq!(objects[1].0, "rsync://example.net/repo/nested/b.roa");
|
|
assert_eq!(objects[1].1, b"b");
|
|
}
|
|
|
|
#[test]
|
|
fn local_dir_rsync_fetcher_reports_read_dir_errors() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let missing = tmp.path().join("missing");
|
|
let f = LocalDirRsyncFetcher::new(missing);
|
|
let err = f.fetch_objects("rsync://example.net/repo").unwrap_err();
|
|
match err {
|
|
RsyncFetchError::Fetch(msg) => assert!(!msg.is_empty()),
|
|
}
|
|
}
|
|
}
|