1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-22 03:05:32 +02:00

feat: entry detail view and artifact file serving

This commit is contained in:
TheGeneralist 2026-06-21 20:42:23 +02:00
parent 9337a822ad
commit dcf9e127bb
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
6 changed files with 868 additions and 10 deletions

View file

@ -278,6 +278,31 @@ pub fn list_runs(conn: &rusqlite::Connection) -> Result<Vec<RunSummary>> {
Ok(runs)
}
/// Resolves an artifact to its absolute on-disk path under `store_path`.
///
/// `artifact.relpath` is a store-relative path (e.g. `raw/a/b/abc.pdf`).
/// The returned path is canonicalized. Returns an error if the resolved path
/// escapes `store_path` (path traversal protection) or if the file does not exist.
pub fn resolve_artifact_path(
store_path: &Path,
artifact: &EntryArtifactSummary,
) -> Result<PathBuf> {
let joined = store_path.join(&artifact.relpath);
let canonical_store = store_path
.canonicalize()
.with_context(|| format!("failed to canonicalize store path: {}", store_path.display()))?;
let canonical_artifact = joined
.canonicalize()
.with_context(|| format!("artifact path does not exist: {}", joined.display()))?;
if !canonical_artifact.starts_with(&canonical_store) {
bail!(
"artifact path escapes store: {}",
canonical_artifact.display()
);
}
Ok(canonical_artifact)
}
#[cfg(test)]
mod tests {
use super::*;
@ -435,4 +460,38 @@ mod tests {
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].status, "completed");
}
#[test]
fn resolve_artifact_path_returns_absolute_path_within_store() {
let root = unique_path("archivr-resolve-artifact");
let store_path = root.join("store");
fs::create_dir_all(store_path.join("raw/a/b")).unwrap();
let artifact_file = store_path.join("raw/a/b/abc.pdf");
fs::write(&artifact_file, b"data").unwrap();
let artifact = EntryArtifactSummary {
artifact_role: "primary".to_string(),
storage_area: "raw".to_string(),
relpath: "raw/a/b/abc.pdf".to_string(),
byte_size: Some(4),
};
let resolved = resolve_artifact_path(&store_path, &artifact).unwrap();
assert_eq!(resolved, artifact_file.canonicalize().unwrap());
let _ = fs::remove_dir_all(&root);
}
#[test]
fn resolve_artifact_path_rejects_traversal() {
let root = unique_path("archivr-resolve-traversal");
let store_path = root.join("store");
fs::create_dir_all(&store_path).unwrap();
let artifact = EntryArtifactSummary {
artifact_role: "primary".to_string(),
storage_area: "raw".to_string(),
relpath: "../escaped.txt".to_string(),
byte_size: None,
};
assert!(resolve_artifact_path(&store_path, &artifact).is_err());
let _ = fs::remove_dir_all(&root);
}
}