use crate::Paths;
use crate::error::{Error, Result};
use crate::scan::ScanReport;
use std::fs;
use std::path::{Path, PathBuf};
pub fn save(paths: &Paths, report: &ScanReport) -> Result<PathBuf> {
let dir = paths.snapshots();
fs::create_dir_all(&dir)?;
let stamp = report.scanned_at.format("%Y-%m-%d-%H%M%S").to_string();
let path = dir.join(format!("{stamp}.json"));
let body = serde_json::to_string_pretty(report)?;
fs::write(&path, body)?;
Ok(path)
}
pub fn load_latest(paths: &Paths) -> Result<Option<ScanReport>> {
let dir = paths.snapshots();
if !dir.exists() {
return Ok(None);
}
let mut newest: Option<PathBuf> = None;
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let p = entry.path();
if p.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
newest = match newest {
Some(cur) if cur >= p => Some(cur),
_ => Some(p),
};
}
match newest {
Some(p) => Ok(Some(load(&p)?)),
None => Ok(None),
}
}
fn load(path: &Path) -> Result<ScanReport> {
let body = fs::read_to_string(path)?;
let report: ScanReport =
serde_json::from_str(&body).map_err(|e| Error::Scan(format!("snapshot parse: {e}")))?;
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scan::inventory::PathEntry;
fn sample() -> ScanReport {
ScanReport {
scanned_at: chrono::Utc::now(),
paths: vec![PathEntry {
path: "/tmp/x".into(),
category: "test".into(),
sha256: "0".repeat(64),
size: 1,
}],
}
}
fn paths_for(tmp: &tempfile::TempDir) -> Paths {
Paths {
home: tmp.path().to_path_buf(),
user_home: tmp.path().to_path_buf(),
}
}
#[test]
fn save_then_load_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let paths = paths_for(&tmp);
let report = sample();
let written = save(&paths, &report).unwrap();
assert!(written.exists());
let loaded = load_latest(&paths).unwrap().unwrap();
assert_eq!(loaded.paths, report.paths);
}
#[test]
fn load_latest_returns_none_when_dir_absent() {
let tmp = tempfile::tempdir().unwrap();
let paths = paths_for(&tmp);
assert!(load_latest(&paths).unwrap().is_none());
}
}