use std::path::Path;
use std::path::PathBuf;
use sha2::Digest;
use sha2::Sha256;
use crate::error::Result;
use crate::error::SnapshotError;
const STORE_DIR: &str = "filesnap";
pub const FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WorkspaceKey(String);
impl WorkspaceKey {
pub fn of(workspace: &Path) -> Result<Self> {
let canonical = workspace
.canonicalize()
.map_err(|e| SnapshotError::io(workspace, e))?;
Ok(Self::of_canonical(&canonical))
}
fn of_canonical(canonical: &Path) -> Self {
let mut hasher = Sha256::new();
hasher.update(canonical.to_string_lossy().as_bytes());
Self(format!("{:x}", hasher.finalize()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
pub fn store_root(data_dir: &Path) -> Result<PathBuf> {
let store = data_dir.join(STORE_DIR);
let root = store.join(format!("v{FORMAT_VERSION}"));
if root.is_dir() {
return Ok(root);
}
if let Some(newer) = newer_version_present(&store)? {
return Err(SnapshotError::UnknownStoreVersion {
path: store,
found: newer,
supported: FORMAT_VERSION,
});
}
std::fs::create_dir_all(&root).map_err(|e| SnapshotError::io(&root, e))?;
Ok(root)
}
fn newer_version_present(store: &Path) -> Result<Option<u32>> {
let entries = match std::fs::read_dir(store) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(SnapshotError::io(store, e)),
};
let mut newest = None;
for entry in entries {
let entry = entry.map_err(|e| SnapshotError::io(store, e))?;
let name = entry.file_name().to_string_lossy().into_owned();
let Some(version) = name.strip_prefix('v').and_then(|n| n.parse::<u32>().ok()) else {
continue;
};
if version > FORMAT_VERSION {
newest = Some(newest.map_or(version, |current: u32| current.max(version)));
}
}
Ok(newest)
}
pub fn partition_dir(root: &Path, key: &WorkspaceKey) -> PathBuf {
root.join("workspaces").join(key.as_str())
}
pub fn blobs_dir(root: &Path) -> PathBuf {
root.join("blobs")
}
pub fn all_partitions(root: &Path) -> Result<Vec<WorkspaceKey>> {
let workspaces = root.join("workspaces");
let entries = match std::fs::read_dir(&workspaces) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(SnapshotError::io(&workspaces, e)),
};
let mut keys = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| SnapshotError::io(&workspaces, e))?;
let name = entry.file_name().to_string_lossy().into_owned();
if name.len() == 64 && name.chars().all(|c| c.is_ascii_hexdigit()) {
keys.push(WorkspaceKey(name));
}
}
keys.sort();
Ok(keys)
}
#[cfg(test)]
#[path = "workspace_tests.rs"]
mod tests;