use std::io::Write as _;
use std::path::Path;
pub use sdivi_snapshot::retention::enforce_retention;
pub use sdivi_snapshot::store::{iso_to_filename_safe, write_snapshot};
use sdivi_snapshot::Snapshot;
fn list_snapshot_entries(dir: &Path) -> std::io::Result<Vec<std::fs::DirEntry>> {
let mut entries: Vec<_> = std::fs::read_dir(dir)?
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name();
let s = name.to_string_lossy();
s.starts_with("snapshot_") && s.ends_with(".json")
})
.collect();
entries.sort_by_key(|e| e.file_name());
Ok(entries)
}
pub fn read_snapshots(dir: &Path) -> std::io::Result<Vec<Snapshot>> {
if !dir.exists() {
return Ok(vec![]);
}
let mut snapshots = Vec::new();
for entry in list_snapshot_entries(dir)? {
let content = std::fs::read_to_string(entry.path())?;
match serde_json::from_str::<Snapshot>(&content) {
Ok(s) => snapshots.push(s),
Err(e) => {
tracing::warn!(
path = %entry.path().display(),
error = %e,
"skipping malformed snapshot"
);
}
}
}
Ok(snapshots)
}
pub fn latest_snapshot(dir: &Path) -> std::io::Result<Option<Snapshot>> {
if !dir.exists() {
return Ok(None);
}
let entries = list_snapshot_entries(dir)?;
match entries.last() {
None => Ok(None),
Some(entry) => {
let content = std::fs::read_to_string(entry.path())?;
let snap = serde_json::from_str(&content)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(Some(snap))
}
}
}
pub fn read_snapshot_by_id(dir: &Path, id: &str) -> std::io::Result<Snapshot> {
let filename = if id.ends_with(".json") {
id.to_string()
} else {
format!("{id}.json")
};
let path = dir.join(&filename);
let content = std::fs::read_to_string(&path)
.map_err(|e| std::io::Error::new(e.kind(), format!("snapshot '{}' not found: {e}", id)))?;
serde_json::from_str(&content)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
pub fn write_boundary_spec(spec: &sdivi_config::BoundarySpec, path: &Path) -> std::io::Result<()> {
if path.exists() {
let existing = std::fs::read_to_string(path)?;
if existing.lines().any(|l| {
let trimmed = l.trim_start();
trimmed.starts_with('#') || trimmed.contains(" #")
}) {
eprintln!(
"sdivi: warning: '{}' contains YAML comments — comments will be lost \
after ratify (see docs/migrating-from-the-python-poc.md)",
path.display()
);
}
}
let yaml = spec.to_yaml();
let parent = path.parent().unwrap_or(Path::new("."));
std::fs::create_dir_all(parent)?;
let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
tmp.write_all(yaml.as_bytes())?;
tmp.persist(path).map_err(|e| e.error)?;
tracing::debug!(path = %path.display(), "boundary spec written");
Ok(())
}