use crate::error::CacheError;
use dig_store::{get_capsule_identity, retrieval_key, CapsuleIdentity};
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
pub const CAPSULES_DIR: &str = "capsules";
pub const TMP_DIR: &str = "tmp";
pub const INDEX_FILE: &str = "index.json";
pub const CAPSULE_EXT: &str = "dig";
pub const PART_EXT: &str = "part";
pub fn capsules_dir(root: &Path) -> PathBuf {
root.join(CAPSULES_DIR)
}
pub fn tmp_dir(root: &Path) -> PathBuf {
root.join(TMP_DIR)
}
pub fn manifest_path(root: &Path) -> PathBuf {
root.join(INDEX_FILE)
}
pub fn capsule_path(root: &Path, key_hex: &str) -> PathBuf {
capsules_dir(root).join(format!("{key_hex}.{CAPSULE_EXT}"))
}
pub fn retrieval_key_hex(id: &CapsuleIdentity) -> String {
let key =
retrieval_key(&id.capsule_urn()).expect("a CapsuleIdentity always yields a valid URN");
hex::encode(key.as_slice())
}
pub fn ensure_dirs(root: &Path) -> Result<(), CacheError> {
fs::create_dir_all(capsules_dir(root))
.and_then(|()| fs::create_dir_all(tmp_dir(root)))
.map_err(|source| CacheError::RootNotWritable {
root: root.to_path_buf(),
source,
})
}
pub fn stage_bytes(root: &Path, bytes: &[u8]) -> Result<PathBuf, CacheError> {
let staged = stage_path(root);
let mut file = File::create(&staged).map_err(|e| CacheError::io(&staged, e))?;
file.write_all(bytes)
.map_err(|e| CacheError::io(&staged, e))?;
file.sync_all().map_err(|e| CacheError::io(&staged, e))?;
Ok(staged)
}
pub fn stage_file(root: &Path, src: &Path) -> Result<(PathBuf, u64), CacheError> {
let staged = stage_path(root);
let mut reader = File::open(src).map_err(|e| CacheError::io(src, e))?;
let mut writer = File::create(&staged).map_err(|e| CacheError::io(&staged, e))?;
let size = std::io::copy(&mut reader, &mut writer).map_err(|e| CacheError::io(&staged, e))?;
writer.sync_all().map_err(|e| CacheError::io(&staged, e))?;
Ok((staged, size))
}
pub fn finalize(root: &Path, key_hex: &str, staged: &Path) -> Result<(), CacheError> {
let dest = capsule_path(root, key_hex);
fs::rename(staged, &dest).map_err(|e| CacheError::io(&dest, e))
}
pub fn discard_staged(staged: &Path) {
let _ = fs::remove_file(staged);
}
fn stage_path(root: &Path) -> PathBuf {
tmp_dir(root).join(format!("{}.{PART_EXT}", uuid::Uuid::new_v4()))
}
pub fn remove_capsule(root: &Path, key_hex: &str) -> Result<(), CacheError> {
let path = capsule_path(root, key_hex);
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(CacheError::io(&path, e)),
}
}
pub fn clean_tmp(root: &Path) -> Result<(), CacheError> {
let dir = tmp_dir(root);
let entries = match fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(CacheError::io(&dir, e)),
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some(PART_EXT) {
let _ = fs::remove_file(&path);
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct ScannedFile {
pub key_hex: String,
pub path: PathBuf,
pub size: u64,
pub mtime_nanos: u128,
}
pub fn scan_capsules(root: &Path) -> Result<Vec<ScannedFile>, CacheError> {
let dir = capsules_dir(root);
let entries = match fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(CacheError::io(&dir, e)),
};
let mut found = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some(CAPSULE_EXT) {
continue;
}
let Some(key_hex) = path.file_stem().and_then(|s| s.to_str()).map(str::to_owned) else {
continue;
};
let meta = entry.metadata().map_err(|e| CacheError::io(&path, e))?;
let mtime_nanos = meta
.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_nanos())
.unwrap_or(0);
found.push(ScannedFile {
key_hex,
path,
size: meta.len(),
mtime_nanos,
});
}
Ok(found)
}
pub fn recover_identity(path: &Path) -> Result<CapsuleIdentity, CacheError> {
let file = File::open(path).map_err(|e| CacheError::io(path, e))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(|e| CacheError::io(path, e))?;
get_capsule_identity(&mmap).map_err(|e| CacheError::CorruptEntry {
path: path.to_path_buf(),
reason: e.to_string(),
})
}