refactor check logic.
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -188,7 +188,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "l-s"
|
name = "l-s"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "l-s"
|
name = "l-s"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
authors = ["licsber <admin@licsber.site>"]
|
authors = ["licsber <admin@licsber.site>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|||||||
43
README.md
43
README.md
@@ -1,40 +1,29 @@
|
|||||||
# l-s
|
# l-s
|
||||||
|
|
||||||
Summary any file‘s meta.
|
为任意文件或文件夹生成和校验元数据(meta 信息)的工具。
|
||||||
|
|
||||||
## 使用说明
|
## 使用说明
|
||||||
|
|
||||||
接收1个命令行参数,为文件/文件夹路径,如命令行参数为空,则默认为当前路径。
|
程序接受一个命令行参数,作为文件或文件夹路径。如果未提供参数,则默认处理当前目录。
|
||||||
|
|
||||||
元数据均以json格式保存,且经过pretty打印,以方便人类阅读。
|
元数据信息以 json 格式保存,且采用美观(pretty)打印以方便阅读。
|
||||||
|
|
||||||
对于输入参数是文件的情况:
|
**如果输入的是文件:**
|
||||||
|
|
||||||
文件的元数据命名与原文件相同,仅新增json拓展名。
|
- 元数据文件与原文件同名,仅添加 `.json` 扩展名,存放在文件所在目录下的 `meta` 子文件夹内(文件夹不存在则自动创建)。
|
||||||
|
- 若不存在相应元数据文件,则自动计算并保存。
|
||||||
|
- 若元数据文件已存在,则仅校验其中的 `xxh128` 哈希值。
|
||||||
|
- 校验通过将提示“校验通过”。
|
||||||
|
- 校验不通过时,会重新计算并输出当前元数据到标准输出,同时保留原元数据文件不变。
|
||||||
|
|
||||||
文件的元数据保存路径是其所在路径下的meta子文件夹,如果元数据保存路径不存在,则新建。
|
**如果输入的是文件夹:**
|
||||||
|
|
||||||
如果文件的元数据不存在,则计算元数据并保存。
|
- 若不存在`meta.json`,则按字母序遍历目录树,计算全部文件元数据并写入`meta.json`。
|
||||||
|
- 若`meta.json`已存在:
|
||||||
如果文件的元数据存在,则对元数据中的xxh128部分进行校验。
|
- 程序会先将其重命名为`meta-old.json`,提示用户正在校验旧数据;
|
||||||
|
- 遍历当前目录结构,仅计算每个文件的`xxh128`并与旧元数据对比;
|
||||||
如果校验通过,则打印校验成功。
|
- 发现校验失败、新增或缺失的文件都会即时打印到标准输出;
|
||||||
|
- 若全部一致,则将`meta-old.json`重命名回`meta.json`;如有异常则保留`meta-old.json`供排查(不会生成新的`meta.json`)。
|
||||||
如果校验不通过,计算当前的元数据并打印到标准输出,保留原元数据文件不动。
|
|
||||||
|
|
||||||
对于输入参数是文件夹的情况:
|
|
||||||
|
|
||||||
遍历这个文件夹,以字母序遍历遇到的每一个子文件夹和文件。
|
|
||||||
|
|
||||||
文件夹的元数据文件就保存在其中,命名为`meta.json`。
|
|
||||||
|
|
||||||
如果文件夹的元数据不存在,则遍历计算并保存。
|
|
||||||
|
|
||||||
如果文件夹的元数据存在,则对原元数据文件重命名为`meta-old.json`,并对其中所有文件的xxh128部分校验。
|
|
||||||
|
|
||||||
遍历如果发现有新增文件或删除文件,都应打印到标准输出提示用户。
|
|
||||||
|
|
||||||
如果全部校验一致,则重新命名回`meta.json`。
|
|
||||||
|
|
||||||
标准`meta.json`的格式如下:
|
标准`meta.json`的格式如下:
|
||||||
|
|
||||||
|
|||||||
97
src/main.rs
97
src/main.rs
@@ -11,7 +11,7 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use meta::{DirSnapshot, FileMeta};
|
use meta::{calc_xxh128, scan_dir_xxh128, DirSnapshot, FileMeta};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
@@ -30,7 +30,6 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn process_file(path: &Path) -> Result<()> {
|
fn process_file(path: &Path) -> Result<()> {
|
||||||
let meta = FileMeta::from_path(path)?;
|
|
||||||
let meta_dir = path
|
let meta_dir = path
|
||||||
.parent()
|
.parent()
|
||||||
.map(Path::to_path_buf)
|
.map(Path::to_path_buf)
|
||||||
@@ -39,8 +38,13 @@ fn process_file(path: &Path) -> Result<()> {
|
|||||||
fs::create_dir_all(&meta_dir)
|
fs::create_dir_all(&meta_dir)
|
||||||
.with_context(|| format!("无法创建目录: {}", meta_dir.display()))?;
|
.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() {
|
if !save_path.exists() {
|
||||||
|
let meta = FileMeta::from_path(path)?;
|
||||||
let json = meta.to_pretty_json()?;
|
let json = meta.to_pretty_json()?;
|
||||||
println!("{}", json);
|
println!("{}", json);
|
||||||
fs::write(&save_path, json)?;
|
fs::write(&save_path, json)?;
|
||||||
@@ -50,48 +54,79 @@ fn process_file(path: &Path) -> Result<()> {
|
|||||||
let existing = File::open(&save_path)
|
let existing = File::open(&save_path)
|
||||||
.with_context(|| format!("无法读取历史元数据: {}", save_path.display()))?;
|
.with_context(|| format!("无法读取历史元数据: {}", save_path.display()))?;
|
||||||
let old_meta = FileMeta::from_reader(existing)?;
|
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!("校验通过.");
|
println!("校验通过.");
|
||||||
} else {
|
return Ok(());
|
||||||
println!("校验失败!");
|
|
||||||
println!("现校验文件:");
|
|
||||||
println!("{}", meta.to_pretty_json()?);
|
|
||||||
println!("原校验文件:");
|
|
||||||
println!("{}", old_meta.to_pretty_json()?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("校验失败!");
|
||||||
|
println!("现校验文件:");
|
||||||
|
let meta = FileMeta::from_path(path)?;
|
||||||
|
println!("{}", meta.to_pretty_json()?);
|
||||||
|
println!("原校验文件:");
|
||||||
|
println!("{}", old_meta.to_pretty_json()?);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_dir(path: &Path) -> Result<()> {
|
fn process_dir(path: &Path) -> Result<()> {
|
||||||
let save_path = path.join("meta.json");
|
let meta_path = path.join("meta.json");
|
||||||
let old_path = path.join("meta-old.json");
|
let backup_path = path.join("meta-old.json");
|
||||||
let has_old = save_path.exists();
|
|
||||||
|
|
||||||
if has_old {
|
if !meta_path.exists() {
|
||||||
if old_path.exists() {
|
let snapshot = DirSnapshot::build_root(path)?;
|
||||||
fs::remove_file(&old_path)?;
|
let json = serde_json::to_string_pretty(&snapshot)?;
|
||||||
}
|
let mut file = File::create(&meta_path)
|
||||||
fs::rename(&save_path, &old_path)
|
.with_context(|| format!("无法写入: {}", meta_path.display()))?;
|
||||||
.with_context(|| format!("无法备份旧文件: {}", save_path.display()))?;
|
file.write_all(json.as_bytes())?;
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapshot = DirSnapshot::build_root(path)?;
|
if backup_path.exists() {
|
||||||
let json = serde_json::to_string_pretty(&snapshot)?;
|
fs::remove_file(&backup_path)?;
|
||||||
let mut file =
|
}
|
||||||
File::create(&save_path).with_context(|| format!("无法写入: {}", save_path.display()))?;
|
|
||||||
file.write_all(json.as_bytes())?;
|
|
||||||
|
|
||||||
if has_old {
|
fs::rename(&meta_path, &backup_path)
|
||||||
let old_meta = FileMeta::from_path(&old_path)?;
|
.with_context(|| format!("无法重命名旧meta: {}", meta_path.display()))?;
|
||||||
let new_meta = FileMeta::from_path(&save_path)?;
|
println!("发现旧元数据,已暂存为 meta-old.json,开始校验...");
|
||||||
if old_meta.matches(&new_meta) {
|
|
||||||
println!("校验通过.");
|
let meta_file =
|
||||||
fs::remove_file(&old_path)?;
|
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 {
|
} 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,13 +111,20 @@ impl FileMeta {
|
|||||||
pub fn to_pretty_json(&self) -> Result<String> {
|
pub fn to_pretty_json(&self) -> Result<String> {
|
||||||
Ok(serde_json::to_string_pretty(self)?)
|
Ok(serde_json::to_string_pretty(self)?)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pub fn matches(&self, other: &Self) -> bool {
|
|
||||||
self.size == other.size
|
pub fn calc_xxh128(path: &Path) -> Result<String> {
|
||||||
&& self.ed2k == other.ed2k
|
let mut file = File::open(path).with_context(|| format!("无法打开文件: {}", path.display()))?;
|
||||||
&& self.md5 == other.md5
|
let mut buffer = vec![0u8; DEFAULT_BUFFER_SIZE];
|
||||||
&& self.sha1 == other.sha1
|
let mut hasher = Xxh3::new();
|
||||||
&& self.sha256 == other.sha256
|
|
||||||
&& self.xxh128 == other.xxh128
|
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()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mod file;
|
mod file;
|
||||||
mod tree;
|
mod tree;
|
||||||
|
|
||||||
pub use file::FileMeta;
|
pub use file::{calc_xxh128, FileMeta};
|
||||||
pub use tree::DirSnapshot;
|
pub use tree::{scan_dir_xxh128, DirSnapshot};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::file::FileMeta;
|
use super::file::{calc_xxh128, FileMeta};
|
||||||
use crate::constants::META_VERSION;
|
use crate::constants::META_VERSION;
|
||||||
use crate::utils::{basename, should_skip_dir, should_skip_file};
|
use crate::utils::{basename, should_skip_dir, should_skip_file};
|
||||||
|
|
||||||
@@ -24,6 +25,10 @@ impl DirSnapshot {
|
|||||||
Ok(node)
|
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> {
|
fn build_node(path: &Path) -> Result<Self> {
|
||||||
let dir_name = path
|
let dir_name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -73,4 +78,61 @@ impl DirSnapshot {
|
|||||||
v: None,
|
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(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user