refactor check logic.

This commit is contained in:
2025-11-21 00:52:15 +08:00
parent bd0c755370
commit 852384a9fc
7 changed files with 166 additions and 73 deletions

View File

@@ -11,7 +11,7 @@ use std::time::Instant;
use anyhow::{Context, Result};
use clap::Parser;
use meta::{DirSnapshot, FileMeta};
use meta::{calc_xxh128, scan_dir_xxh128, DirSnapshot, FileMeta};
fn main() -> Result<()> {
let started = Instant::now();
@@ -30,7 +30,6 @@ fn main() -> Result<()> {
}
fn process_file(path: &Path) -> Result<()> {
let meta = FileMeta::from_path(path)?;
let meta_dir = path
.parent()
.map(Path::to_path_buf)
@@ -39,8 +38,13 @@ fn process_file(path: &Path) -> Result<()> {
fs::create_dir_all(&meta_dir)
.with_context(|| format!("无法创建目录: {}", meta_dir.display()))?;
let save_path = meta_dir.join(format!("{}.json", meta.basename));
let basename = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
let save_path = meta_dir.join(format!("{basename}.json"));
if !save_path.exists() {
let meta = FileMeta::from_path(path)?;
let json = meta.to_pretty_json()?;
println!("{}", json);
fs::write(&save_path, json)?;
@@ -50,48 +54,79 @@ fn process_file(path: &Path) -> Result<()> {
let existing = File::open(&save_path)
.with_context(|| format!("无法读取历史元数据: {}", save_path.display()))?;
let old_meta = FileMeta::from_reader(existing)?;
if meta.matches(&old_meta) {
let fast_hash = calc_xxh128(path)?;
if fast_hash == old_meta.xxh128 {
println!("校验通过.");
} else {
println!("校验失败!");
println!("现校验文件:");
println!("{}", meta.to_pretty_json()?);
println!("原校验文件:");
println!("{}", old_meta.to_pretty_json()?);
return Ok(());
}
println!("校验失败!");
println!("现校验文件:");
let meta = FileMeta::from_path(path)?;
println!("{}", meta.to_pretty_json()?);
println!("原校验文件:");
println!("{}", old_meta.to_pretty_json()?);
Ok(())
}
fn process_dir(path: &Path) -> Result<()> {
let save_path = path.join("meta.json");
let old_path = path.join("meta-old.json");
let has_old = save_path.exists();
let meta_path = path.join("meta.json");
let backup_path = path.join("meta-old.json");
if has_old {
if old_path.exists() {
fs::remove_file(&old_path)?;
}
fs::rename(&save_path, &old_path)
.with_context(|| format!("无法备份旧文件: {}", save_path.display()))?;
if !meta_path.exists() {
let snapshot = DirSnapshot::build_root(path)?;
let json = serde_json::to_string_pretty(&snapshot)?;
let mut file = File::create(&meta_path)
.with_context(|| format!("无法写入: {}", meta_path.display()))?;
file.write_all(json.as_bytes())?;
return Ok(());
}
let snapshot = DirSnapshot::build_root(path)?;
let json = serde_json::to_string_pretty(&snapshot)?;
let mut file =
File::create(&save_path).with_context(|| format!("无法写入: {}", save_path.display()))?;
file.write_all(json.as_bytes())?;
if backup_path.exists() {
fs::remove_file(&backup_path)?;
}
if has_old {
let old_meta = FileMeta::from_path(&old_path)?;
let new_meta = FileMeta::from_path(&save_path)?;
if old_meta.matches(&new_meta) {
println!("校验通过.");
fs::remove_file(&old_path)?;
fs::rename(&meta_path, &backup_path)
.with_context(|| format!("无法重命名旧meta: {}", meta_path.display()))?;
println!("发现旧元数据,已暂存为 meta-old.json开始校验...");
let meta_file =
File::open(&backup_path).with_context(|| format!("无法读取: {}", backup_path.display()))?;
let snapshot = DirSnapshot::from_reader(meta_file)?;
let mut stored = snapshot.collect_file_map(path);
let current = scan_dir_xxh128(path)?;
let mut issues = false;
for (file_path, hash) in current {
if let Some(meta) = stored.remove(&file_path) {
if hash != meta.xxh128 {
println!(
"校验失败: {}\n 期望: {}\n 当前: {}",
file_path.display(),
meta.xxh128,
hash
);
issues = true;
}
} else {
println!("校验失败!");
println!("文件新增: {}", file_path.display());
issues = true;
}
}
for (missing_path, _) in stored {
println!("文件缺失: {}", missing_path.display());
issues = true;
}
if issues {
println!("校验存在异常,已保留 meta-old.json 供排查。");
} else {
println!("校验通过.");
fs::rename(&backup_path, &meta_path)
.with_context(|| format!("无法恢复meta: {}", meta_path.display()))?;
}
Ok(())
}

View File

@@ -111,13 +111,20 @@ impl FileMeta {
pub fn to_pretty_json(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn matches(&self, other: &Self) -> bool {
self.size == other.size
&& self.ed2k == other.ed2k
&& self.md5 == other.md5
&& self.sha1 == other.sha1
&& self.sha256 == other.sha256
&& self.xxh128 == other.xxh128
}
}
pub fn calc_xxh128(path: &Path) -> Result<String> {
let mut file = File::open(path).with_context(|| format!("无法打开文件: {}", path.display()))?;
let mut buffer = vec![0u8; DEFAULT_BUFFER_SIZE];
let mut hasher = Xxh3::new();
loop {
let read_len = file.read(&mut buffer)?;
if read_len == 0 {
break;
}
hasher.update(&buffer[..read_len]);
}
Ok(hex_upper(hasher.digest128().to_be_bytes()))
}

View File

@@ -1,5 +1,5 @@
mod file;
mod tree;
pub use file::FileMeta;
pub use tree::DirSnapshot;
pub use file::{calc_xxh128, FileMeta};
pub use tree::{scan_dir_xxh128, DirSnapshot};

View File

@@ -1,10 +1,11 @@
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use super::file::FileMeta;
use super::file::{calc_xxh128, FileMeta};
use crate::constants::META_VERSION;
use crate::utils::{basename, should_skip_dir, should_skip_file};
@@ -24,6 +25,10 @@ impl DirSnapshot {
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) -> Result<Self> {
let dir_name = path
.file_name()
@@ -73,4 +78,61 @@ impl DirSnapshot {
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);
}
}
}
pub fn scan_dir_xxh128(path: &Path) -> Result<BTreeMap<PathBuf, String>> {
let mut map = BTreeMap::new();
walk_dir(path, &mut map)?;
Ok(map)
}
fn walk_dir(path: &Path, map: &mut BTreeMap<PathBuf, String>) -> Result<()> {
let mut entries = fs::read_dir(path)
.with_context(|| format!("无法遍历目录: {}", path.display()))?
.collect::<Result<Vec<_>, _>>()
.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(())
}