mod cli; mod constants; mod head_hash; mod meta; mod utils; use std::fs::{self, File, OpenOptions}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::time::{Instant, SystemTime, UNIX_EPOCH}; use anyhow::{anyhow, Context, Result}; use clap::Parser; use meta::{ calc_xxh128_from_file_with_callback, open_regular_file_nofollow, 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()); let target_info = fs::symlink_metadata(&target) .with_context(|| format!("无法读取路径信息: {}", target.display()))?; if target_info.is_dir() { process_dir(&target)?; } else if target_info.is_file() { process_file(&target)?; } else { return Err(anyhow!("{} 不是文件或目录", target.display())); } 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"); match symlink_metadata_optional(&meta_dir)? { Some(info) if info.file_type().is_symlink() => { return Err(anyhow!("不支持符号链接目录: {}", meta_dir.display())); } Some(info) if !info.is_dir() => { return Err(anyhow!("{} 不是目录", meta_dir.display())); } Some(_) => {} None => { 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 existing_save = symlink_metadata_optional(&save_path)?; if existing_save.is_none() { let file = open_regular_file_nofollow(path)?; let file_size = file .metadata() .with_context(|| format!("无法读取文件信息: {}", path.display()))? .len(); 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_open_file_with_callback(path, file, on_bytes, on_iop)?; tracker.finish("处理完成"); let json = meta.to_pretty_json()?; println!("{}", json); write_atomic(&save_path, &json)?; return Ok(()); } let save_info = existing_save.expect("checked as Some"); if save_info.file_type().is_symlink() { return Err(anyhow!("不支持符号链接元数据文件: {}", save_path.display())); } if !save_info.is_file() { return Err(anyhow!("{} 不是文件", save_path.display())); } let existing = open_regular_file_nofollow(&save_path) .with_context(|| format!("无法读取历史元数据: {}", save_path.display()))?; let old_meta = FileMeta::from_reader(existing)?; // 使用进度条计算快速哈希 let file = open_regular_file_nofollow(path)?; let file_size = file .metadata() .with_context(|| format!("无法读取文件信息: {}", path.display()))? .len(); 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_from_file_with_callback(path, file, on_bytes, on_iop)?; tracker.finish("校验完成"); if fast_hash == old_meta.xxh128 { println!("校验通过."); return Ok(()); } println!("校验失败!"); println!("现校验文件:"); let file = open_regular_file_nofollow(path)?; let file_size = file .metadata() .with_context(|| format!("无法读取文件信息: {}", path.display()))? .len(); 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_open_file_with_callback(path, file, 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"); let Some(meta_info) = symlink_metadata_optional(&meta_path)? else { let snapshot = DirSnapshot::build_root(path)?; let json = serde_json::to_string_pretty(&snapshot)?; write_atomic(&meta_path, &json)?; return Ok(()); }; if meta_info.file_type().is_symlink() { return Err(anyhow!("不支持符号链接元数据文件: {}", meta_path.display())); } if !meta_info.is_file() { return Err(anyhow!("{} 不是文件", meta_path.display())); } if symlink_metadata_optional(&backup_path)?.is_some() { fs::remove_file(&backup_path)?; } fs::rename(&meta_path, &backup_path) .with_context(|| format!("无法重命名旧meta: {}", meta_path.display()))?; sync_parent_dir(&backup_path)?; println!("发现旧元数据,已暂存为 meta-old.json,开始校验..."); let meta_file = open_regular_file_nofollow(&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()))?; sync_parent_dir(&meta_path)?; } 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 temp = OpenOptions::new() .write(true) .create_new(true) .open(&tmp_path) .with_context(|| format!("无法创建临时文件: {}", tmp_path.display()))?; temp.write_all(contents.as_bytes()) .with_context(|| format!("无法写入临时文件: {}", tmp_path.display()))?; temp.sync_all() .with_context(|| format!("无法同步临时文件: {}", tmp_path.display()))?; drop(temp); fs::rename(&tmp_path, path).with_context(|| { format!( "无法将临时文件重命名为目标文件: {} -> {}", tmp_path.display(), path.display() ) })?; sync_parent_dir(path)?; Ok(()) })(); if result.is_err() { let _ = fs::remove_file(&tmp_path); } result } fn symlink_metadata_optional(path: &Path) -> Result> { match fs::symlink_metadata(path) { Ok(info) => Ok(Some(info)), Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), Err(err) => Err(err).with_context(|| format!("无法读取路径信息: {}", path.display())), } } fn sync_parent_dir(path: &Path) -> Result<()> { let parent = path.parent().unwrap_or_else(|| Path::new(".")); let dir = File::open(parent) .with_context(|| format!("无法打开父目录以同步: {}", parent.display()))?; dir.sync_all() .with_context(|| format!("无法同步父目录: {}", parent.display())) }