use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::BundleRelativePath;
use crate::transaction::filesystem::FileSystem;
use crate::transaction::{SnapshotId, TransactionError, TransactionOptions, WritePlan};
#[derive(Debug)]
pub(crate) struct SnapshotStore<'a> {
filesystem: FileSystem,
options: &'a TransactionOptions,
}
impl<'a> SnapshotStore<'a> {
pub(crate) const fn new(filesystem: FileSystem, options: &'a TransactionOptions) -> Self {
Self {
filesystem,
options,
}
}
pub(crate) fn create(
&self,
snapshot_id: SnapshotId,
plan: &WritePlan,
) -> Result<(), TransactionError> {
let directory = self.directory(snapshot_id);
let files_root = directory.join("files");
FileSystem::create_private_dir(&directory)?;
FileSystem::create_private_dir(&files_root)?;
let mut entries = Vec::with_capacity(plan.len());
for write in plan.writes() {
let target = self.options.root().join(write.path.as_path());
let existed = target.is_file();
let (mode, sha256) = if existed {
let backup = files_root.join(write.path.as_path());
if let Some(parent) = backup.parent() {
FileSystem::create_private_dir(parent)?;
}
FileSystem::copy(&target, &backup)?;
let content = FileSystem::read(&backup)?;
(
file_mode(&target)?,
Some(hex::encode(Sha256::digest(content))),
)
} else {
(None, None)
};
entries.push(SnapshotEntry {
path: write.path.clone(),
existed,
mode,
sha256,
});
}
let manifest = serde_json::to_vec_pretty(&SnapshotManifest { entries })
.map_err(TransactionError::InvalidSnapshot)?;
FileSystem::write(&directory.join("manifest.json"), &manifest)
}
pub(crate) fn restore(&self, snapshot_id: SnapshotId) -> Result<(), TransactionError> {
let directory = self.directory(snapshot_id);
let manifest_path = directory.join("manifest.json");
if !manifest_path.is_file() {
return Err(TransactionError::SnapshotNotFound(snapshot_id));
}
let manifest_bytes = FileSystem::read(&manifest_path)?;
let manifest: SnapshotManifest =
serde_json::from_slice(&manifest_bytes).map_err(TransactionError::InvalidSnapshot)?;
for entry in manifest.entries {
FileSystem::ensure_path_safe(self.options.root(), entry.path.as_path())?;
let target = self.options.root().join(entry.path.as_path());
if entry.existed {
let backup = directory.join("files").join(entry.path.as_path());
let content = FileSystem::read(&backup)?;
if entry.sha256.as_deref() != Some(&hex::encode(Sha256::digest(&content))) {
return Err(TransactionError::SnapshotHashMismatch { path: backup });
}
self.filesystem.atomic_write(
self.options.root(),
&target,
&content,
entry.mode.unwrap_or(0o600),
)?;
} else if target.is_file() {
FileSystem::remove_file(&target)?;
if let Some(parent) = target.parent() {
FileSystem::remove_empty_parents(self.options.root(), parent)?;
}
}
}
Ok(())
}
fn directory(&self, snapshot_id: SnapshotId) -> PathBuf {
self.options
.state_root()
.join("backups")
.join(snapshot_id.to_string())
}
}
#[derive(Debug, Serialize, Deserialize)]
struct SnapshotManifest {
entries: Vec<SnapshotEntry>,
}
#[derive(Debug, Serialize, Deserialize)]
struct SnapshotEntry {
path: BundleRelativePath,
existed: bool,
mode: Option<u32>,
sha256: Option<String>,
}
#[cfg(unix)]
fn file_mode(path: &std::path::Path) -> Result<Option<u32>, TransactionError> {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.map(|metadata| Some(metadata.permissions().mode() & 0o777))
.map_err(|source| TransactionError::Io {
path: path.to_path_buf(),
source,
})
}
#[cfg(not(unix))]
fn file_mode(_path: &std::path::Path) -> Result<Option<u32>, TransactionError> {
Ok(None)
}