use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use super::file::{calc_xxh128, FileMeta}; 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, #[serde(skip_serializing_if = "Option::is_none")] pub v: Option, } impl DirSnapshot { pub fn build_root(path: &Path) -> Result { let mut node = Self::build_node(path)?; node.v = Some(META_VERSION.to_string()); Ok(node) } pub fn from_reader(reader: R) -> Result { Ok(serde_json::from_reader(reader)?) } fn build_node(path: &Path) -> 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_by(|a, b| a.file_name().cmp(&b.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_dir() { if should_skip_dir(&name) { continue; } println!("目录: {}", full_path.display()); dirs.push(Self::build_node(&full_path)?); continue; } if should_skip_file(&name) { continue; } let meta = FileMeta::from_path(&full_path)?; println!("文件: {} {}", meta.friendly_size, full_path.display()); files.push(meta); } 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); } } } pub fn scan_dir_xxh128(path: &Path) -> Result> { let mut map = BTreeMap::new(); walk_dir(path, &mut map)?; Ok(map) } fn walk_dir(path: &Path, map: &mut BTreeMap) -> Result<()> { let mut entries = fs::read_dir(path) .with_context(|| format!("无法遍历目录: {}", path.display()))? .collect::, _>>() .with_context(|| format!("读取目录失败: {}", path.display()))?; entries.sort_by(|a, b| a.file_name().cmp(&b.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_dir() { if should_skip_dir(&name) { continue; } walk_dir(&full_path, map)?; continue; } if should_skip_file(&name) { continue; } let hash = calc_xxh128(&full_path)?; map.insert(full_path, hash); } Ok(()) }