From d7da1c325f91fa4156519cc48e86821e7bdbbcb1 Mon Sep 17 00:00:00 2001 From: licsber Date: Fri, 3 Apr 2026 21:22:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=200.5.0=20=E6=96=B0=E5=A2=9E=E5=AD=90?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E5=A2=9E=E9=87=8F=E6=A0=A1=E9=AA=8C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 当子目录存在 meta.json 时,直接进行 xxh128 快速校验而非重新计算 - 添加详细的错误提示(校验失败/新增文件/文件缺失) - ProgressTracker 添加 multi() 方法,支持 MultiProgress::suspend 协同输出 - 优化排序:sort_by → sort_unstable_by_key - 版本号更新至 0.5.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/meta/progress.rs | 13 ++++++---- src/meta/tree.rs | 60 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b677335..7feb365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,7 +236,7 @@ dependencies = [ [[package]] name = "l-s" -version = "0.4.1" +version = "0.5.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 585e62f..c6fe857 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "l-s" -version = "0.4.1" +version = "0.5.0" authors = ["licsber "] edition = "2021" diff --git a/src/meta/progress.rs b/src/meta/progress.rs index b4761dd..bdc5bb0 100644 --- a/src/meta/progress.rs +++ b/src/meta/progress.rs @@ -8,9 +8,7 @@ use crate::utils::friendly_size; /// 进度跟踪器,封装进度条和 IO 统计信息 pub struct ProgressTracker { - // MultiProgress 必须保持存活,否则进度条会消失 - #[allow(dead_code)] - _multi: Option, + multi: Option, file_progress_bar: Option, // 文件数量进度条 current_file_bar: Option, // 当前文件进度条 bytes_read: Arc, @@ -54,7 +52,7 @@ impl ProgressTracker { }; Self { - _multi: multi, + multi, file_progress_bar, current_file_bar, bytes_read: Arc::new(AtomicU64::new(0)), @@ -82,7 +80,7 @@ impl ProgressTracker { current_pb.set_message(format!("处理: {}", file_name)); Self { - _multi: Some(multi), + multi: Some(multi), file_progress_bar: None, current_file_bar: Some(current_pb), bytes_read: Arc::new(AtomicU64::new(0)), @@ -265,4 +263,9 @@ impl ProgressTracker { iops.fetch_add(1, Ordering::Relaxed); } } + + /// 获取 MultiProgress,用于 `println` / `suspend` 等与进度条协同输出 + pub fn multi(&self) -> Option<&MultiProgress> { + self.multi.as_ref() + } } diff --git a/src/meta/tree.rs b/src/meta/tree.rs index 7d7c94d..2bb6d9e 100644 --- a/src/meta/tree.rs +++ b/src/meta/tree.rs @@ -1,8 +1,9 @@ use std::collections::BTreeMap; use std::fs; +use std::fs::File; use std::path::{Path, PathBuf}; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; use super::file::{calc_xxh128_with_callback, FileMeta}; @@ -15,6 +16,7 @@ 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, } @@ -49,7 +51,7 @@ impl DirSnapshot { .collect::, _>>() .with_context(|| format!("读取目录失败: {}", path.display()))?; - entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + entries.sort_unstable_by_key(|e| e.file_name()); for entry in entries { let file_name = entry.file_name(); @@ -67,7 +69,12 @@ impl DirSnapshot { if should_skip_dir(&name) { continue; } - dirs.push(Self::build_node(&full_path, tracker)?); + 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; } @@ -112,6 +119,51 @@ impl DirSnapshot { 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> { @@ -173,7 +225,7 @@ fn walk_dir_with_progress( .with_context(|| format!("无法遍历目录: {}", path.display()))? .collect::, _>>() .with_context(|| format!("读取目录失败: {}", path.display()))?; - entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + entries.sort_unstable_by_key(|e| e.file_name()); for entry in entries { let file_name = entry.file_name();