use camino::Utf8Path;
use same_file::Handle;
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AbsorbDecision {
InSync,
RelinkOnly,
AutoAbsorb,
NeedsConfirm,
Restore,
}
pub fn classify(source: &Utf8Path, target: &Utf8Path) -> Result<AbsorbDecision> {
let target_meta = match std::fs::symlink_metadata(target) {
Ok(m) => m,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(AbsorbDecision::Restore);
}
Err(e) => return Err(e.into()),
};
if let (Ok(src_h), Ok(dst_h)) = (
Handle::from_path(source.as_std_path()),
Handle::from_path(target.as_std_path()),
) {
if src_h == dst_h {
return Ok(AbsorbDecision::InSync);
}
}
let source_meta = std::fs::metadata(source)?;
if target_meta.file_type().is_dir() && source_meta.file_type().is_dir() {
return Ok(AbsorbDecision::NeedsConfirm);
}
if target_meta.file_type().is_file() && source_meta.file_type().is_file() {
let identical = source_meta.len() == target_meta.len()
&& std::fs::read(source)? == std::fs::read(target)?;
if identical {
return Ok(AbsorbDecision::RelinkOnly);
}
let src_mtime = source_meta.modified()?;
let dst_mtime = target_meta.modified()?;
if dst_mtime > src_mtime {
return Ok(AbsorbDecision::AutoAbsorb);
}
return Ok(AbsorbDecision::NeedsConfirm);
}
Ok(AbsorbDecision::NeedsConfirm)
}
#[cfg(test)]
mod tests {
use super::*;
use camino::Utf8PathBuf;
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
fn utf8(p: std::path::PathBuf) -> Utf8PathBuf {
Utf8PathBuf::from_path_buf(p).unwrap()
}
fn backdate(path: &Utf8Path, when: SystemTime) {
let f = std::fs::OpenOptions::new()
.write(true)
.open(path)
.expect("open writable for set_modified");
f.set_modified(when).expect("set_modified");
}
#[test]
fn missing_target_is_restore() {
let tmp = TempDir::new().unwrap();
let src = utf8(tmp.path().join("src.txt"));
std::fs::write(&src, "x").unwrap();
let dst = utf8(tmp.path().join("dst.txt"));
assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::Restore);
}
#[test]
fn hardlink_is_in_sync() {
let tmp = TempDir::new().unwrap();
let src = utf8(tmp.path().join("src.txt"));
std::fs::write(&src, "x").unwrap();
let dst = utf8(tmp.path().join("dst.txt"));
std::fs::hard_link(&src, &dst).unwrap();
assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::InSync);
}
#[test]
fn separate_files_same_content_is_relink_only() {
let tmp = TempDir::new().unwrap();
let src = utf8(tmp.path().join("src.txt"));
let dst = utf8(tmp.path().join("dst.txt"));
std::fs::write(&src, "same body").unwrap();
std::fs::write(&dst, "same body").unwrap();
assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::RelinkOnly);
}
#[test]
fn target_newer_with_diff_is_auto_absorb() {
let tmp = TempDir::new().unwrap();
let src = utf8(tmp.path().join("src.txt"));
let dst = utf8(tmp.path().join("dst.txt"));
std::fs::write(&src, "old source").unwrap();
let past = SystemTime::now() - Duration::from_secs(60);
backdate(&src, past);
std::fs::write(&dst, "edited target").unwrap();
assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::AutoAbsorb);
}
#[test]
fn source_newer_with_diff_is_needs_confirm() {
let tmp = TempDir::new().unwrap();
let src = utf8(tmp.path().join("src.txt"));
let dst = utf8(tmp.path().join("dst.txt"));
std::fs::write(&dst, "old target").unwrap();
let past = SystemTime::now() - Duration::from_secs(60);
backdate(&dst, past);
std::fs::write(&src, "fresh source").unwrap();
assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::NeedsConfirm);
}
#[test]
fn separate_dirs_are_needs_confirm() {
let tmp = TempDir::new().unwrap();
let src = utf8(tmp.path().join("src_dir"));
let dst = utf8(tmp.path().join("dst_dir"));
std::fs::create_dir_all(&src).unwrap();
std::fs::create_dir_all(&dst).unwrap();
assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::NeedsConfirm);
}
}