Files
l-s/src/meta/tree.rs
licsber d7da1c325f feat: 0.5.0 新增子目录增量校验功能
- 当子目录存在 meta.json 时,直接进行 xxh128 快速校验而非重新计算
- 添加详细的错误提示(校验失败/新增文件/文件缺失)
- ProgressTracker 添加 multi() 方法,支持 MultiProgress::suspend 协同输出
- 优化排序:sort_by → sort_unstable_by_key
- 版本号更新至 0.5.0
2026-04-03 21:22:12 +08:00

269 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<DirSnapshot>,
pub files: Vec<FileMeta>,
// 如果 Option::is_none即该字段为 None序列化为 JSON 时会跳过(不输出)该字段
#[serde(skip_serializing_if = "Option::is_none")]
pub v: Option<String>,
}
impl DirSnapshot {
pub fn build_root(path: &Path) -> Result<Self> {
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<R: std::io::Read>(reader: R) -> Result<Self> {
Ok(serde_json::from_reader(reader)?)
}
fn build_node(path: &Path, tracker: &ProgressTracker) -> Result<Self> {
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::<Result<Vec<_>, _>>()
.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<PathBuf, FileMeta> {
let mut map = BTreeMap::new();
self.collect_into(root.to_path_buf(), &mut map);
map
}
fn collect_into(&self, current: PathBuf, map: &mut BTreeMap<PathBuf, FileMeta>) {
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<Self> {
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<BTreeMap<PathBuf, String>> {
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<u64> {
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::<Result<Vec<_>, _>>()
.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<PathBuf, String>,
tracker: &ProgressTracker,
) -> Result<()> {
let mut entries = fs::read_dir(path)
.with_context(|| format!("无法遍历目录: {}", path.display()))?
.collect::<Result<Vec<_>, _>>()
.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(())
}