use std::collections::BTreeMap; use std::fs; use std::fs::File; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; use super::file::{calc_xxh128_with_callback, FileMeta}; use super::progress::ProgressTracker; use crate::constants::META_VERSION; use crate::utils::{basename, should_skip_dir, should_skip_file}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DirSnapshot { pub dir_name: String, pub dirs: Vec, pub files: Vec, // 如果 Option::is_none(即该字段为 None),序列化为 JSON 时会跳过(不输出)该字段 #[serde(skip_serializing_if = "Option::is_none")] pub v: Option, } impl DirSnapshot { pub fn build_root(path: &Path) -> Result { let total_files = count_files(path)?; let tracker = ProgressTracker::new(total_files, "构建中..."); let mut node = Self::build_node(path, &tracker)?; node.v = Some(META_VERSION.to_string()); tracker.finish("构建完成"); Ok(node) } pub fn from_reader(reader: R) -> Result { Ok(serde_json::from_reader(reader)?) } fn build_node(path: &Path, tracker: &ProgressTracker) -> Result { let dir_name = path .file_name() .map(basename) .unwrap_or_else(|| path.to_string_lossy().to_string()); let mut dirs = Vec::new(); let mut files = Vec::new(); let mut entries = fs::read_dir(path) .with_context(|| format!("无法遍历目录: {}", path.display()))? .collect::, _>>() .with_context(|| format!("读取目录失败: {}", path.display()))?; entries.sort_unstable_by_key(|e| e.file_name()); for entry in entries { let file_name = entry.file_name(); let name = file_name.to_string_lossy().to_string(); let full_path = entry.path(); let file_type = entry .file_type() .with_context(|| format!("无法读取类型: {}", full_path.display()))?; if file_type.is_symlink() { continue; } if file_type.is_dir() { if should_skip_dir(&name) { continue; } let sub_meta = full_path.join("meta.json"); if sub_meta.exists() { dirs.push(Self::verify_and_load(&full_path, tracker)?); } else { dirs.push(Self::build_node(&full_path, tracker)?); } continue; } if should_skip_file(&name) { continue; } // 获取文件大小并开始跟踪 let file_size = entry.metadata() .map(|m| m.len()) .unwrap_or(0); tracker.start_file(file_size, &name); let on_bytes = tracker.bytes_callback(); let on_iop = tracker.iop_callback(); let meta = FileMeta::from_path_with_callback(&full_path, on_bytes, on_iop)?; files.push(meta); tracker.finish_file(); } Ok(Self { dir_name, dirs, files, v: None, }) } pub fn collect_file_map(&self, root: &Path) -> BTreeMap { let mut map = BTreeMap::new(); self.collect_into(root.to_path_buf(), &mut map); map } fn collect_into(&self, current: PathBuf, map: &mut BTreeMap) { for file in &self.files { map.insert(current.join(&file.basename), file.clone()); } for dir in &self.dirs { let next = current.join(&dir.dir_name); dir.collect_into(next, map); } } /// 加载子目录的 meta.json 并通过 xxh128 快速校验。 /// 校验通过则返回已有的 DirSnapshot,否则返回 Err 终止流程。 fn verify_and_load(path: &Path, tracker: &ProgressTracker) -> Result { let meta_path = path.join("meta.json"); let meta_file = File::open(&meta_path) .with_context(|| format!("无法读取: {}", meta_path.display()))?; let snapshot: Self = serde_json::from_reader(meta_file) .with_context(|| format!("无法解析: {}", meta_path.display()))?; let mut stored = snapshot.collect_file_map(path); let mut current = BTreeMap::new(); walk_dir_with_progress(path, &mut current, tracker)?; for (file_path, hash) in current { if let Some(meta) = stored.remove(&file_path) { if hash != meta.xxh128 { return Err(anyhow!( "校验失败: {}\n 期望: {}\n 当前: {}", file_path.display(), meta.xxh128, hash )); } } else { return Err(anyhow!("文件新增: {}", file_path.display())); } } if let Some((missing_path, _)) = stored.into_iter().next() { return Err(anyhow!("文件缺失: {}", missing_path.display())); } // 须通过 MultiProgress::suspend:先清屏进度区再打印,否则 stderr 上的独立 eprintln // 会被下一次 tick 的光标移动覆盖,看起来像「只有进度条在往上刷」。 let msg = format!("✓ 校验通过: {}", path.display()); if let Some(multi) = tracker.multi() { multi.suspend(|| { eprintln!("{msg}"); }); } else { eprintln!("{msg}"); } Ok(snapshot) } } pub fn scan_dir_xxh128(path: &Path) -> Result> { let total_files = count_files(path)?; let tracker = ProgressTracker::new(total_files, "扫描中..."); let mut map = BTreeMap::new(); walk_dir_with_progress(path, &mut map, &tracker)?; tracker.finish("扫描完成"); Ok(map) } fn count_files(path: &Path) -> Result { let mut count = 0u64; count_files_recursive(path, &mut count)?; Ok(count) } fn count_files_recursive(path: &Path, count: &mut u64) -> Result<()> { let entries = fs::read_dir(path) .with_context(|| format!("无法遍历目录: {}", path.display()))? .collect::, _>>() .with_context(|| format!("读取目录失败: {}", path.display()))?; for entry in entries { let file_name = entry.file_name(); let name = file_name.to_string_lossy().to_string(); let full_path = entry.path(); let file_type = entry .file_type() .with_context(|| format!("无法读取类型: {}", full_path.display()))?; if file_type.is_symlink() { continue; } if file_type.is_dir() { if should_skip_dir(&name) { continue; } count_files_recursive(&full_path, count)?; } else { if !should_skip_file(&name) { *count += 1; } } } Ok(()) } fn walk_dir_with_progress( path: &Path, map: &mut BTreeMap, tracker: &ProgressTracker, ) -> Result<()> { let mut entries = fs::read_dir(path) .with_context(|| format!("无法遍历目录: {}", path.display()))? .collect::, _>>() .with_context(|| format!("读取目录失败: {}", path.display()))?; entries.sort_unstable_by_key(|e| e.file_name()); for entry in entries { let file_name = entry.file_name(); let name = file_name.to_string_lossy().to_string(); let full_path = entry.path(); let file_type = entry .file_type() .with_context(|| format!("无法读取类型: {}", full_path.display()))?; if file_type.is_symlink() { continue; } if file_type.is_dir() { if should_skip_dir(&name) { continue; } walk_dir_with_progress(&full_path, map, tracker)?; continue; } if should_skip_file(&name) { continue; } // 获取文件大小并开始跟踪 let file_size = entry.metadata() .map(|m| m.len()) .unwrap_or(0); tracker.start_file(file_size, &name); let on_bytes = tracker.bytes_callback(); let on_iop = tracker.iop_callback(); let hash = calc_xxh128_with_callback(&full_path, on_bytes, on_iop)?; map.insert(full_path, hash); tracker.finish_file(); } Ok(()) }