use std::collections::BTreeMap;
use std::path::Path;
use std::time::UNIX_EPOCH;
use anyhow::Context as _;
use serde::{Deserialize, Serialize};
pub(crate) const SAVED_MANIFEST_NAME: &str = ".decdn-manifest.json";
const SAVED_MANIFEST_VERSION: u32 = 1;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub(crate) struct SavedManifest {
version: u32,
files: BTreeMap<String, SavedFile>,
}
impl SavedManifest {
pub(crate) fn get(&self, path: &str) -> Option<&SavedFile> {
self.files.get(path)
}
pub(crate) fn records(&self) -> impl Iterator<Item = (&str, &SavedFile)> {
self.files.iter().map(|(p, r)| (p.as_str(), r))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SavedFile {
pub(crate) hash: String,
pub(crate) size: u64,
pub(crate) mtime: SavedMtime,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) chunks: Option<Vec<SavedChunk>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SavedChunk {
pub(crate) hash: String,
pub(crate) size: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SavedMtime {
pub(crate) secs: u64,
pub(crate) nanos: u32,
}
impl SavedMtime {
pub(crate) fn of(meta: &std::fs::Metadata) -> Option<SavedMtime> {
let d = meta.modified().ok()?.duration_since(UNIX_EPOCH).ok()?;
Some(SavedMtime {
secs: d.as_secs(),
nanos: d.subsec_nanos(),
})
}
}
pub(crate) fn saved_hints(rec: &SavedFile) -> Option<Vec<(String, u64, u64)>> {
let chunks = rec.chunks.as_ref()?;
let mut out = Vec::with_capacity(chunks.len());
let mut offset = 0u64;
for c in chunks {
out.push((c.hash.clone(), offset, c.size));
offset = offset.checked_add(c.size)?;
}
(offset == rec.size).then_some(out)
}
pub(crate) fn load(out_root: &Path) -> SavedManifest {
let path = out_root.join(SAVED_MANIFEST_NAME);
let Ok(bytes) = std::fs::read(&path) else {
return SavedManifest::default();
};
match serde_json::from_slice::<SavedManifest>(&bytes) {
Ok(m) if m.version == SAVED_MANIFEST_VERSION => m,
_ => SavedManifest::default(),
}
}
pub(crate) fn merge(manifest: &mut SavedManifest, updates: BTreeMap<String, SavedFile>) {
manifest.version = SAVED_MANIFEST_VERSION;
for (path, rec) in updates {
manifest.files.insert(path, rec);
}
}
pub(crate) fn serialize(manifest: &SavedManifest) -> anyhow::Result<Vec<u8>> {
serde_json::to_vec_pretty(manifest).context("serialize saved manifest")
}
pub(crate) fn write_bytes(out_root: &Path, bytes: &[u8]) -> anyhow::Result<()> {
let dest = out_root.join(SAVED_MANIFEST_NAME);
let mut tmp = super::engine::temp_in_parent(&dest).context("stage saved manifest")?;
std::io::Write::write_all(tmp.as_file_mut(), bytes).context("write saved manifest")?;
tmp.as_file().sync_all().context("sync saved manifest")?;
tmp.persist(&dest)
.map_err(|e| e.error)
.context("persist saved manifest")?;
Ok(())
}
#[cfg(test)]
pub(crate) fn merge_and_write(
out_root: &Path,
mut prior: SavedManifest,
updates: BTreeMap<String, SavedFile>,
) -> anyhow::Result<()> {
merge(&mut prior, updates);
let bytes = serialize(&prior)?;
write_bytes(out_root, &bytes)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
mod tests;