use std::collections::BTreeMap;
use std::path::Path;
use std::time::UNIX_EPOCH;
use anyhow::Context as _;
use serde::{Deserialize, Serialize};
use crate::uri::{DecdnUri, UriTarget};
pub(crate) const SAVED_MANIFEST_NAME: &str = ".decdn-manifest.json";
const SAVED_MANIFEST_VERSION: u32 = 2;
#[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>>,
pub(crate) source: SavedSource,
}
impl SavedFile {
pub(crate) fn still_matches(&self, meta: &std::fs::Metadata) -> bool {
meta.is_file() && meta.len() == self.size && SavedMtime::of(meta) == Some(self.mtime)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SavedSource {
pub(crate) bundle: String,
pub(crate) path: String,
}
impl SavedSource {
pub(crate) fn new(bundle: [u8; 32], path: &str) -> Self {
Self {
bundle: bundle_key(bundle),
path: path.to_owned(),
}
}
pub(crate) fn is(&self, bundle: [u8; 32], path: &str) -> bool {
self.path == path && super::parse_hash(&self.bundle).is_ok_and(|h| h == bundle)
}
pub(crate) fn uri(&self) -> Option<DecdnUri> {
let hash = super::parse_hash(&self.bundle).ok()?;
Some(DecdnUri::new(
hash,
UriTarget::BundleEntry(self.path.clone()),
None,
))
}
}
fn bundle_key(hash: [u8; 32]) -> String {
format!("b3:{}", blake3::Hash::from_bytes(hash).to_hex())
}
#[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 {
load_checked(out_root).unwrap_or_default()
}
pub(crate) fn load_checked(out_root: &Path) -> anyhow::Result<SavedManifest> {
#[derive(Deserialize)]
struct Versioned {
version: u32,
}
let path = out_root.join(SAVED_MANIFEST_NAME);
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(SavedManifest::default());
}
Err(e) => return Err(e).with_context(|| format!("read {}", path.display())),
};
let Versioned { version } = serde_json::from_slice(&bytes)
.with_context(|| format!("{} is not a skip-cache", path.display()))?;
anyhow::ensure!(
version == SAVED_MANIFEST_VERSION,
"{} has version {version}; this build reads version {SAVED_MANIFEST_VERSION}",
path.display()
);
serde_json::from_slice(&bytes)
.with_context(|| format!("{} is not a skip-cache", path.display()))
}
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;