feat: 0.5.0 新增子目录增量校验功能
- 当子目录存在 meta.json 时,直接进行 xxh128 快速校验而非重新计算 - 添加详细的错误提示(校验失败/新增文件/文件缺失) - ProgressTracker 添加 multi() 方法,支持 MultiProgress::suspend 协同输出 - 优化排序:sort_by → sort_unstable_by_key - 版本号更新至 0.5.0
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -236,7 +236,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "l-s"
|
name = "l-s"
|
||||||
version = "0.4.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "l-s"
|
name = "l-s"
|
||||||
version = "0.4.1"
|
version = "0.5.0"
|
||||||
authors = ["licsber <admin@licsber.site>"]
|
authors = ["licsber <admin@licsber.site>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ use crate::utils::friendly_size;
|
|||||||
|
|
||||||
/// 进度跟踪器,封装进度条和 IO 统计信息
|
/// 进度跟踪器,封装进度条和 IO 统计信息
|
||||||
pub struct ProgressTracker {
|
pub struct ProgressTracker {
|
||||||
// MultiProgress 必须保持存活,否则进度条会消失
|
multi: Option<MultiProgress>,
|
||||||
#[allow(dead_code)]
|
|
||||||
_multi: Option<MultiProgress>,
|
|
||||||
file_progress_bar: Option<ProgressBar>, // 文件数量进度条
|
file_progress_bar: Option<ProgressBar>, // 文件数量进度条
|
||||||
current_file_bar: Option<ProgressBar>, // 当前文件进度条
|
current_file_bar: Option<ProgressBar>, // 当前文件进度条
|
||||||
bytes_read: Arc<AtomicU64>,
|
bytes_read: Arc<AtomicU64>,
|
||||||
@@ -54,7 +52,7 @@ impl ProgressTracker {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
_multi: multi,
|
multi,
|
||||||
file_progress_bar,
|
file_progress_bar,
|
||||||
current_file_bar,
|
current_file_bar,
|
||||||
bytes_read: Arc::new(AtomicU64::new(0)),
|
bytes_read: Arc::new(AtomicU64::new(0)),
|
||||||
@@ -82,7 +80,7 @@ impl ProgressTracker {
|
|||||||
current_pb.set_message(format!("处理: {}", file_name));
|
current_pb.set_message(format!("处理: {}", file_name));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
_multi: Some(multi),
|
multi: Some(multi),
|
||||||
file_progress_bar: None,
|
file_progress_bar: None,
|
||||||
current_file_bar: Some(current_pb),
|
current_file_bar: Some(current_pb),
|
||||||
bytes_read: Arc::new(AtomicU64::new(0)),
|
bytes_read: Arc::new(AtomicU64::new(0)),
|
||||||
@@ -265,4 +263,9 @@ impl ProgressTracker {
|
|||||||
iops.fetch_add(1, Ordering::Relaxed);
|
iops.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取 MultiProgress,用于 `println` / `suspend` 等与进度条协同输出
|
||||||
|
pub fn multi(&self) -> Option<&MultiProgress> {
|
||||||
|
self.multi.as_ref()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::fs::File;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::file::{calc_xxh128_with_callback, FileMeta};
|
use super::file::{calc_xxh128_with_callback, FileMeta};
|
||||||
@@ -15,6 +16,7 @@ pub struct DirSnapshot {
|
|||||||
pub dir_name: String,
|
pub dir_name: String,
|
||||||
pub dirs: Vec<DirSnapshot>,
|
pub dirs: Vec<DirSnapshot>,
|
||||||
pub files: Vec<FileMeta>,
|
pub files: Vec<FileMeta>,
|
||||||
|
// 如果 Option::is_none(即该字段为 None),序列化为 JSON 时会跳过(不输出)该字段
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub v: Option<String>,
|
pub v: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -49,7 +51,7 @@ impl DirSnapshot {
|
|||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.with_context(|| format!("读取目录失败: {}", path.display()))?;
|
.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 {
|
for entry in entries {
|
||||||
let file_name = entry.file_name();
|
let file_name = entry.file_name();
|
||||||
@@ -67,7 +69,12 @@ impl DirSnapshot {
|
|||||||
if should_skip_dir(&name) {
|
if should_skip_dir(&name) {
|
||||||
continue;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +119,51 @@ impl DirSnapshot {
|
|||||||
dir.collect_into(next, map);
|
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>> {
|
pub fn scan_dir_xxh128(path: &Path) -> Result<BTreeMap<PathBuf, String>> {
|
||||||
@@ -173,7 +225,7 @@ fn walk_dir_with_progress(
|
|||||||
.with_context(|| format!("无法遍历目录: {}", path.display()))?
|
.with_context(|| format!("无法遍历目录: {}", path.display()))?
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.with_context(|| format!("读取目录失败: {}", path.display()))?;
|
.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 {
|
for entry in entries {
|
||||||
let file_name = entry.file_name();
|
let file_name = entry.file_name();
|
||||||
|
|||||||
Reference in New Issue
Block a user