mod cli;
mod constants;
mod head_hash;
mod meta;
mod utils;
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result};
use clap::Parser;
use meta::{calc_xxh128_with_callback, scan_dir_xxh128, DirSnapshot, FileMeta, ProgressTracker};
fn main() -> Result<()> {
let started = Instant::now();
let cli = cli::Cli::parse();
let target = cli.resolve_path()?;
println!("目标: {}", target.display());
if target.is_dir() {
process_dir(&target)?;
} else {
process_file(&target)?;
}
println!("耗时: {:?}", started.elapsed());
Ok(())
}
fn process_file(path: &Path) -> Result<()> {
let meta_dir = path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."))
.join("meta");
fs::create_dir_all(&meta_dir)
.with_context(|| format!("无法创建目录: {}", meta_dir.display()))?;
let basename = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
let save_path = meta_dir.join(format!("{basename}.json"));
let file_size = fs::metadata(path)
.with_context(|| format!("无法读取文件信息: {}", path.display()))?
.len();
if !save_path.exists() {
let tracker = ProgressTracker::new_single_file(file_size, &basename);
let on_bytes = tracker.bytes_callback();
let on_iop = tracker.iop_callback();
let meta = FileMeta::from_path_with_callback(path, on_bytes, on_iop)?;
tracker.finish("处理完成");
let json = meta.to_pretty_json()?;
println!("{}", json);
write_atomic(&save_path, &json)?;
return Ok(());
}
let existing = File::open(&save_path)
.with_context(|| format!("无法读取历史元数据: {}", save_path.display()))?;
let old_meta = FileMeta::from_reader(existing)?;
let tracker = ProgressTracker::new_single_file(file_size, &basename);
let on_bytes = tracker.bytes_callback();
let on_iop = tracker.iop_callback();
let fast_hash = calc_xxh128_with_callback(path, on_bytes, on_iop)?;
tracker.finish("校验完成");
if fast_hash == old_meta.xxh128 {
println!("校验通过.");
return Ok(());
}
println!("校验失败!");
println!("现校验文件:");
let tracker = ProgressTracker::new_single_file(file_size, &basename);
let on_bytes = tracker.bytes_callback();
let on_iop = tracker.iop_callback();
let meta = FileMeta::from_path_with_callback(path, on_bytes, on_iop)?;
tracker.finish("处理完成");
println!("{}", meta.to_pretty_json()?);
println!("原校验文件:");
println!("{}", old_meta.to_pretty_json()?);
Ok(())
}
fn process_dir(path: &Path) -> Result<()> {
let meta_path = path.join("meta.json");
let backup_path = path.join("meta-old.json");
if !meta_path.exists() {
let snapshot = DirSnapshot::build_root(path)?;
let json = serde_json::to_string_pretty(&snapshot)?;
write_atomic(&meta_path, &json)?;
return Ok(());
}
if backup_path.exists() {
fs::remove_file(&backup_path)?;
}
fs::rename(&meta_path, &backup_path)
.with_context(|| format!("无法重命名旧meta: {}", meta_path.display()))?;
println!("发现旧元数据,已暂存为 meta-old.json,开始校验...");
let meta_file =
File::open(&backup_path).with_context(|| format!("无法读取: {}", backup_path.display()))?;
let snapshot = DirSnapshot::from_reader(meta_file)?;
let mut stored = snapshot.collect_file_map(path);
let current = scan_dir_xxh128(path)?;
let mut issues = false;
for (file_path, hash) in current {
if let Some(meta) = stored.remove(&file_path) {
if hash != meta.xxh128 {
println!(
"校验失败: {}\n 期望: {}\n 当前: {}",
file_path.display(),
meta.xxh128,
hash
);
issues = true;
}
} else {
println!("文件新增: {}", file_path.display());
issues = true;
}
}
for (missing_path, _) in stored {
println!("文件缺失: {}", missing_path.display());
issues = true;
}
if issues {
println!("校验存在异常,已保留 meta-old.json 供排查。");
} else {
println!("校验通过.");
fs::rename(&backup_path, &meta_path)
.with_context(|| format!("无法恢复meta: {}", meta_path.display()))?;
}
Ok(())
}
fn write_atomic(path: &Path, contents: &str) -> Result<()> {
let parent = path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or(0);
let tmp_path = parent.join(format!(".l-s-tmp-{}-{nanos}", std::process::id()));
let result = (|| -> Result<()> {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp_path)
.with_context(|| format!("无法创建临时文件: {}", tmp_path.display()))?;
file.write_all(contents.as_bytes())
.with_context(|| format!("无法写入临时文件: {}", tmp_path.display()))?;
file.sync_all()
.with_context(|| format!("无法同步临时文件: {}", tmp_path.display()))?;
drop(file);
fs::rename(&tmp_path, path).with_context(|| {
format!(
"无法将临时文件重命名为目标文件: {} -> {}",
tmp_path.display(),
path.display()
)
})?;
Ok(())
})();
if result.is_err() {
let _ = fs::remove_file(&tmp_path);
}
result
}