use std::path::Path;
use super::{
ActionOutcome, DedupPlan, DedupReport, FileSnapshot, JournalAction, JournalOp, LinkStrategy,
SkippedCopy,
};
use crate::paths::ShelfPaths;
use crate::registry::{LocationRole, RegistryStore};
use crate::{Error, Result};
pub(crate) fn apply(
paths: &ShelfPaths,
store: &RegistryStore,
plan: &DedupPlan,
) -> Result<DedupReport> {
let op_id = format!("{}-{}", crate::time_util::now_compact(), std::process::id());
let mut journal = JournalOp {
op_id: op_id.clone(),
created_at: crate::time_util::now_rfc3339(),
strategy: plan.strategy,
actions: plan
.actions
.iter()
.map(|a| JournalAction {
action: a.clone(),
outcome: ActionOutcome::Skipped, reason: Some("not attempted".into()),
})
.collect(),
};
write_journal(paths, &journal)?;
let mut report = DedupReport {
op_id: op_id.clone(),
..Default::default()
};
for adopt in &plan.adoptions {
if let Err(e) = crate::store::adopt(paths, &adopt.source, &adopt.sha256_hex) {
tracing::warn!(id = %adopt.id, "adoption failed: {e}");
}
}
for (i, action) in plan.actions.iter().enumerate() {
let result = apply_one(action, plan.strategy);
match result {
Ok(()) => {
journal.actions[i].outcome = ActionOutcome::Applied;
journal.actions[i].reason = None;
report.applied += 1;
report.reclaimed_bytes += action.snapshot.size_bytes;
let target = action.target.clone();
let id = action.id.clone();
let role = match plan.strategy {
LinkStrategy::Hardlink => LocationRole::Hardlink,
LinkStrategy::Symlink => LocationRole::Symlink,
};
let _ = store.update(|reg| {
if let Some(entry) = reg.find_mut(&id) {
if let Some(loc) = entry.locations.iter_mut().find(|l| l.path == target) {
loc.role = role;
}
}
Ok(())
});
}
Err(e) => {
journal.actions[i].outcome = ActionOutcome::Skipped;
journal.actions[i].reason = Some(e.to_string());
report.skipped.push(SkippedCopy {
path: action.target.clone(),
reason: e.to_string(),
});
}
}
write_journal(paths, &journal)?;
}
Ok(report)
}
fn apply_one(action: &super::DedupAction, strategy: LinkStrategy) -> Result<()> {
if !action.blob.is_file() {
return Err(Error::Refused("store blob missing".into()));
}
let current = FileSnapshot::of(&action.target)?;
if current != action.snapshot {
return Err(Error::Refused(
"file changed since planning (size/mtime/inode mismatch)".into(),
));
}
let tmp = swap_tmp_path(&action.target);
let _ = std::fs::remove_file(&tmp); let link_result = match strategy {
LinkStrategy::Hardlink => std::fs::hard_link(&action.blob, &tmp),
LinkStrategy::Symlink => symlink(&action.blob, &tmp),
};
link_result.map_err(|e| Error::io(&tmp, e))?;
if let Err(e) = std::fs::rename(&tmp, &action.target) {
let _ = std::fs::remove_file(&tmp);
return Err(Error::io(&action.target, e));
}
Ok(())
}
pub(crate) fn swap_tmp_path(target: &Path) -> std::path::PathBuf {
let mut name = target
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
name.push_str(".msh-tmp");
target.with_file_name(name)
}
fn symlink(original: &Path, link: &Path) -> std::io::Result<()> {
#[cfg(unix)]
return std::os::unix::fs::symlink(original, link);
#[cfg(windows)]
return std::os::windows::fs::symlink_file(original, link);
#[cfg(not(any(unix, windows)))]
{
let _ = (original, link);
Err(std::io::Error::other(
"symlinks unsupported on this platform",
))
}
}
pub(crate) fn journal_path(paths: &ShelfPaths, op_id: &str) -> std::path::PathBuf {
paths.journal_dir().join(format!("{op_id}.json"))
}
pub(crate) fn write_journal(paths: &ShelfPaths, journal: &JournalOp) -> Result<()> {
let path = journal_path(paths, &journal.op_id);
let bytes = serde_json::to_vec_pretty(journal)
.map_err(|e| Error::io(&path, std::io::Error::other(e)))?;
crate::registry::lock::write_atomic(&path, &bytes)
}
pub(crate) fn read_journal(paths: &ShelfPaths, op_id: &str) -> Result<JournalOp> {
let path = journal_path(paths, op_id);
let bytes = std::fs::read(&path).map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => Error::NotFound(format!("no journal for op {op_id}")),
_ => Error::io(&path, e),
})?;
serde_json::from_slice(&bytes).map_err(|e| Error::io(&path, std::io::Error::other(e)))
}