most done.

This commit is contained in:
2025-11-21 00:14:11 +08:00
parent 6ab9c38955
commit bd0c755370
12 changed files with 877 additions and 28 deletions

2
.gitignore vendored
View File

@@ -1 +1 @@
/target
target/

387
Cargo.lock generated
View File

@@ -1,7 +1,390 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "ed2k"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f994dfc4b4a48e6230596a86a2de4dfa988f67e2c6caf6c5888d27bfe3e8a378"
dependencies = [
"digest",
"md4",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "l-s"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"anyhow",
"clap",
"ed2k",
"md-5",
"serde",
"serde_json",
"sha1",
"sha2",
"xxhash-rust",
]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "md4"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da5ac363534dce5fabf69949225e174fbf111a498bf0ff794c8ea1fba9f3dda"
dependencies = [
"digest",
]
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "xxhash-rust"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"

View File

@@ -1,6 +1,6 @@
[package]
name = "l-s"
version = "0.1.0"
version = "0.1.1"
authors = ["licsber <admin@licsber.site>"]
edition = "2021"
@@ -11,3 +11,12 @@ readme = "README.md"
license = "AGPL-3.0-only"
[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
ed2k = "1.0.1"
md-5 = "0.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha1 = "0.10"
sha2 = "0.10"
xxhash-rust = { version = "0.8", features = ["xxh3"] }

View File

@@ -43,21 +43,21 @@ Summary any files meta.
"dir_name": "test",
"dirs": [
{
"dir_name": "j",
"dir_name": "child",
"dirs": [],
"files": [
{
"basename": "CW-NAS-J6-230729.iso",
"size": 20000768,
"friendly_size": "19.07MB",
"mtime": 1763445083,
"head_115": "0F62212861F9AB6213A793AA74B33C8856AA9D45",
"head_baidu": "FE3C25567C3681EEAA1EAAE7804460CF",
"ed2k": "D3F1330B892884240142296E0A670EE4",
"md5": "AB14F900F1ABBF6A723EB8F7D6DDABC1",
"sha1": "B1D31D15A866EE0C3DCC9972FB92EE6FA2BE4D4A",
"sha256": "E217F816094C5EE4CF366E6F168851C13B27D8CC9C42F392B637CAD1C20A8510",
"xxh128": "F4A47B42150480BE1193EA7DA0DFD083"
"basename": "233",
"size": 4,
"friendly_size": "4.00B",
"mtime": 1763654099,
"head_115": "28AAB5A575FA1138E2CE5B1366AE697685775011",
"head_baidu": "1490AAA92CB684B2110DDB29D7A1AC15",
"ed2k": "6CB03133656BDB8DFC780EBBD4FF47CC",
"md5": "9F3D9739B11C2A4B08EA48512AC467F6",
"sha1": "10E25C6EC9A30C731BF82E5DBA37BC693E9F615D",
"sha256": "5F8064636753265C7F1B1EE075DF77E1AE9BCE7E94831DE583784A0C13EB902F",
"xxh128": "B92C6051418D532F7E933C08C44C4C88"
}
]
}

26
src/cli.rs Normal file
View File

@@ -0,0 +1,26 @@
use std::path::PathBuf;
use std::{env, fs};
use anyhow::{Context, Result};
use clap::Parser;
/// 负责解析命令行参数
#[derive(Debug, Parser)]
// 这个宏定义命令行工具的信息,比如作者、版本和描述,用于帮助信息输出
#[command(author, version, about = "汇总文件或文件夹的元信息")]
pub struct Cli {
/// 需要扫描的目标路径,默认当前工作目录
pub path: Option<PathBuf>,
}
impl Cli {
pub fn resolve_path(&self) -> Result<PathBuf> {
let candidate = match &self.path {
Some(p) => p.clone(),
None => env::current_dir().context("无法获取当前工作目录")?,
};
fs::canonicalize(&candidate)
.with_context(|| format!("无法解析路径: {}", candidate.display()))
}
}

22
src/constants.rs Normal file
View File

@@ -0,0 +1,22 @@
pub const DEFAULT_BUFFER_SIZE: usize = 4 * 1024 * 1024;
pub const HEAD_115_BYTES: usize = 128 * 1024;
pub const HEAD_BAIDU_BYTES: usize = 256 * 1024;
pub const META_VERSION: &str = "2025-11-21";
pub const SKIP_DIR_NAMES: &[&str] = &[
"@Recently-Snapshot",
"@Recycle",
".@__thumb",
"@Transcode",
"meta",
"$RECYCLE.BIN",
];
pub const SKIP_FILE_NAMES: &[&str] = &[
".DS_Store",
"licsber-bak.json",
"meta.json",
"meta-old.json",
"Thumbs.db",
"desktop.ini",
];

43
src/head_hash.rs Normal file
View File

@@ -0,0 +1,43 @@
use md5::{digest::Digest, Md5};
use sha1::Sha1;
use crate::utils::hex_upper;
pub struct HeadChunk {
buffer: Vec<u8>,
filled: usize,
}
impl HeadChunk {
pub fn new(size: usize) -> Self {
Self {
buffer: vec![0u8; size],
filled: 0,
}
}
pub fn feed(&mut self, data: &[u8]) {
if self.filled >= self.buffer.len() {
return;
}
let take = (self.buffer.len() - self.filled).min(data.len());
self.buffer[self.filled..self.filled + take].copy_from_slice(&data[..take]);
self.filled += take;
}
pub fn as_slice(&self) -> &[u8] {
&self.buffer
}
}
pub fn calc_head_115(chunk: &[u8]) -> String {
let mut hasher = Sha1::default();
hasher.update(chunk);
hex_upper(hasher.finalize())
}
pub fn calc_head_baidu(chunk: &[u8]) -> String {
let mut hasher = Md5::default();
hasher.update(chunk);
hex_upper(hasher.finalize())
}

View File

@@ -1,3 +1,97 @@
fn main() {
println!("Hello, world!");
mod cli;
mod constants;
mod head_hash;
mod meta;
mod utils;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::{Context, Result};
use clap::Parser;
use meta::{DirSnapshot, FileMeta};
fn main() -> Result<()> {
let started = Instant::now();
let cli = cli::Cli::parse();
let target = cli.resolve_path()?;
println!("目标: {}", target.display());
if target.is_dir() {
process_dir(&target)?;
} else {
process_file(&target)?;
}
println!("耗时: {:?}", started.elapsed());
Ok(())
}
fn process_file(path: &Path) -> Result<()> {
let meta = FileMeta::from_path(path)?;
let meta_dir = path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."))
.join("meta");
fs::create_dir_all(&meta_dir)
.with_context(|| format!("无法创建目录: {}", meta_dir.display()))?;
let save_path = meta_dir.join(format!("{}.json", meta.basename));
if !save_path.exists() {
let json = meta.to_pretty_json()?;
println!("{}", json);
fs::write(&save_path, json)?;
return Ok(());
}
let existing = File::open(&save_path)
.with_context(|| format!("无法读取历史元数据: {}", save_path.display()))?;
let old_meta = FileMeta::from_reader(existing)?;
if meta.matches(&old_meta) {
println!("校验通过.");
} else {
println!("校验失败!");
println!("现校验文件:");
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();
if has_old {
if old_path.exists() {
fs::remove_file(&old_path)?;
}
fs::rename(&save_path, &old_path)
.with_context(|| format!("无法备份旧文件: {}", save_path.display()))?;
}
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 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)?;
} else {
println!("校验失败!");
}
}
Ok(())
}

123
src/meta/file.rs Normal file
View File

@@ -0,0 +1,123 @@
use std::fs::{self, File};
use std::io::Read;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{anyhow, Context, Result};
use ed2k::digest::Digest;
use ed2k::Ed2k;
use md5::Md5;
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use sha2::Sha256;
use xxhash_rust::xxh3::Xxh3;
use crate::constants::{DEFAULT_BUFFER_SIZE, HEAD_115_BYTES, HEAD_BAIDU_BYTES};
use crate::head_hash::{calc_head_115, calc_head_baidu, HeadChunk};
use crate::utils::{basename, friendly_size, hex_upper};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMeta {
pub basename: String,
pub size: u64,
pub friendly_size: String,
pub mtime: i64,
pub head_115: String,
pub head_baidu: String,
pub ed2k: String,
pub md5: String,
pub sha1: String,
pub sha256: String,
pub xxh128: String,
}
impl FileMeta {
pub fn from_path(path: &Path) -> Result<Self> {
let info =
fs::metadata(path).with_context(|| format!("无法读取文件信息: {}", path.display()))?;
if !info.is_file() {
return Err(anyhow!("{} 不是文件", path.display()));
}
let basename_str = basename(
path.file_name()
.ok_or_else(|| anyhow!("{} 缺少文件名", path.display()))?,
);
let size = info.len();
let friendly = friendly_size(size);
let mtime = info
.modified()
.unwrap_or(SystemTime::UNIX_EPOCH)
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let mut file =
File::open(path).with_context(|| format!("无法打开文件: {}", path.display()))?;
let mut buffer = vec![0u8; DEFAULT_BUFFER_SIZE];
let mut md5_hasher = Md5::new();
let mut sha1_hasher = Sha1::new();
let mut sha256_hasher = Sha256::new();
let mut xxh_hasher = Xxh3::new();
let mut ed2k_hasher = Ed2k::new();
let mut head115 = HeadChunk::new(HEAD_115_BYTES);
let mut head_baidu = HeadChunk::new(HEAD_BAIDU_BYTES);
loop {
let read_len = file.read(&mut buffer)?;
if read_len == 0 {
break;
}
let chunk = &buffer[..read_len];
md5_hasher.update(chunk);
sha1_hasher.update(chunk);
sha256_hasher.update(chunk);
xxh_hasher.update(chunk);
ed2k_hasher.update(chunk);
head115.feed(chunk);
head_baidu.feed(chunk);
}
let head_115 = calc_head_115(head115.as_slice());
let head_baidu = calc_head_baidu(head_baidu.as_slice());
let md5_hex = hex_upper(md5_hasher.finalize());
let sha1_hex = hex_upper(sha1_hasher.finalize());
let sha256_hex = hex_upper(sha256_hasher.finalize());
let xxh_hex = hex_upper(xxh_hasher.digest128().to_be_bytes());
let ed2k_hex = hex_upper(ed2k_hasher.finalize());
Ok(Self {
basename: basename_str,
size,
friendly_size: friendly,
mtime,
head_115,
head_baidu,
ed2k: ed2k_hex,
md5: md5_hex,
sha1: sha1_hex,
sha256: sha256_hex,
xxh128: xxh_hex,
})
}
pub fn from_reader<R: Read>(reader: R) -> Result<Self> {
Ok(serde_json::from_reader(reader)?)
}
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
}
}

5
src/meta/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
mod file;
mod tree;
pub use file::FileMeta;
pub use tree::DirSnapshot;

76
src/meta/tree.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::fs;
use std::path::Path;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use super::file::FileMeta;
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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub v: Option<String>,
}
impl DirSnapshot {
pub fn build_root(path: &Path) -> Result<Self> {
let mut node = Self::build_node(path)?;
node.v = Some(META_VERSION.to_string());
Ok(node)
}
fn build_node(path: &Path) -> 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_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;
}
println!("目录: {}", full_path.display());
dirs.push(Self::build_node(&full_path)?);
continue;
}
if should_skip_file(&name) {
continue;
}
let meta = FileMeta::from_path(&full_path)?;
println!("文件: {} {}", meta.friendly_size, full_path.display());
files.push(meta);
}
Ok(Self {
dir_name,
dirs,
files,
v: None,
})
}
}

68
src/utils.rs Normal file
View File

@@ -0,0 +1,68 @@
use std::ffi::OsStr;
use crate::constants::{SKIP_DIR_NAMES, SKIP_FILE_NAMES};
pub fn friendly_size(size: u64) -> String {
const UNITS: [(&str, u64); 5] = [
("B", 1),
("KB", 1024),
("MB", 1024 * 1024),
("GB", 1024 * 1024 * 1024),
("TB", 1024 * 1024 * 1024 * 1024),
];
if size == 0 {
return "0B".to_string();
}
let mut value = size as f64;
let mut unit = "B";
for (label, threshold) in UNITS.iter().rev() {
if size >= *threshold {
value = size as f64 / *threshold as f64;
unit = label;
break;
}
}
format!("{:.2}{}", value, unit)
}
pub fn basename(path: &OsStr) -> String {
path.to_string_lossy().to_string()
}
pub fn should_skip_dir(name: &str) -> bool {
SKIP_DIR_NAMES
.iter()
.any(|item| item.eq_ignore_ascii_case(name))
}
pub fn should_skip_file(name: &str) -> bool {
SKIP_FILE_NAMES
.iter()
.any(|item| item.eq_ignore_ascii_case(name))
|| name.starts_with("._")
|| name.starts_with("Thumb_")
}
pub fn hex_upper(bytes: impl AsRef<[u8]>) -> String {
bytes
.as_ref()
.iter()
.map(|b| format!("{:02X}", b))
.collect::<String>()
}
#[cfg(test)]
mod tests {
use super::friendly_size;
#[test]
fn friendly_size_formats_units() {
assert_eq!(friendly_size(0), "0B");
assert_eq!(friendly_size(1), "1.00B");
assert_eq!(friendly_size(1024), "1.00KB");
assert_eq!(friendly_size(1024 * 1024), "1.00MB");
}
}