use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use super::{state::Baseline, LeanResult, CONTROL_DIR, STATE_DIR};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScanEntry {
pub size: u64,
pub mtime_unix: i64,
pub mtime_nanos: u32,
pub mode: u32,
}
pub const TMP_SUFFIX: &str = ".flint-sync-tmp";
pub fn stat_changed(b_size: u64, b_mtime: i64, b_nanos: Option<u32>, size: u64, mtime: i64, nanos: u32) -> bool {
b_size != size || b_mtime != mtime || b_nanos.map(|n| n != nanos).unwrap_or(false)
}
pub fn scan(root: &Path) -> LeanResult<BTreeMap<String, ScanEntry>> {
let mut out = BTreeMap::new();
walk(root, root, &mut out)?;
Ok(out)
}
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<String, ScanEntry>) -> LeanResult<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let name = entry.file_name();
if dir == root {
let n = name.to_string_lossy();
if n == STATE_DIR || n == CONTROL_DIR {
continue;
}
}
if name.to_string_lossy().ends_with(TMP_SUFFIX) {
continue;
}
let meta = std::fs::symlink_metadata(&path)?;
if meta.file_type().is_symlink() {
continue;
}
if meta.is_dir() {
walk(root, &path, out)?;
} else if meta.is_file() {
let rel = path
.strip_prefix(root)
.expect("walk stays under root")
.to_string_lossy()
.replace('\\', "/");
#[cfg(unix)]
let mode = {
use std::os::unix::fs::MetadataExt;
meta.mode()
};
#[cfg(not(unix))]
let mode = 0o644;
let (mtime_unix, mtime_nanos) = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| (d.as_secs() as i64, d.subsec_nanos()))
.unwrap_or((0, 0));
out.insert(rel, ScanEntry { size: meta.len(), mtime_unix, mtime_nanos, mode });
}
}
Ok(())
}
#[derive(Debug, Default)]
pub struct Classified {
pub uploads: BTreeSet<String>,
pub deletes: BTreeSet<String>,
pub first_absence: BTreeSet<String>,
}
pub fn is_control_path(path: &str) -> bool {
[CONTROL_DIR, STATE_DIR].iter().any(|d| {
path == *d || path.strip_prefix(d).map(|r| r.starts_with('/')).unwrap_or(false)
})
}
pub fn classify(scan: &BTreeMap<String, ScanEntry>, baseline: &Baseline) -> Classified {
let mut c = Classified::default();
for (path, s) in scan {
match baseline.entries.get(path) {
None => {
c.uploads.insert(path.clone());
}
Some(b) => {
if stat_changed(b.size, b.mtime_unix, b.mtime_nanos, s.size, s.mtime_unix, s.mtime_nanos) {
c.uploads.insert(path.clone());
}
}
}
}
for path in baseline.entries.keys() {
if scan.contains_key(path) {
continue;
}
if is_control_path(path) {
continue;
}
if baseline.prev_scan.contains(path) {
c.first_absence.insert(path.clone());
} else {
c.deletes.insert(path.clone());
}
}
c
}