most done.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1 @@
|
|||||||
/target
|
target/
|
||||||
|
|||||||
387
Cargo.lock
generated
387
Cargo.lock
generated
@@ -1,7 +1,390 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# 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]]
|
[[package]]
|
||||||
name = "l-s"
|
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"
|
||||||
|
|||||||
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "l-s"
|
name = "l-s"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
authors = ["licsber <admin@licsber.site>"]
|
authors = ["licsber <admin@licsber.site>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -11,3 +11,12 @@ readme = "README.md"
|
|||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[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"] }
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -43,21 +43,21 @@ Summary any file‘s meta.
|
|||||||
"dir_name": "test",
|
"dir_name": "test",
|
||||||
"dirs": [
|
"dirs": [
|
||||||
{
|
{
|
||||||
"dir_name": "j",
|
"dir_name": "child",
|
||||||
"dirs": [],
|
"dirs": [],
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"basename": "CW-NAS-J6-230729.iso",
|
"basename": "233",
|
||||||
"size": 20000768,
|
"size": 4,
|
||||||
"friendly_size": "19.07MB",
|
"friendly_size": "4.00B",
|
||||||
"mtime": 1763445083,
|
"mtime": 1763654099,
|
||||||
"head_115": "0F62212861F9AB6213A793AA74B33C8856AA9D45",
|
"head_115": "28AAB5A575FA1138E2CE5B1366AE697685775011",
|
||||||
"head_baidu": "FE3C25567C3681EEAA1EAAE7804460CF",
|
"head_baidu": "1490AAA92CB684B2110DDB29D7A1AC15",
|
||||||
"ed2k": "D3F1330B892884240142296E0A670EE4",
|
"ed2k": "6CB03133656BDB8DFC780EBBD4FF47CC",
|
||||||
"md5": "AB14F900F1ABBF6A723EB8F7D6DDABC1",
|
"md5": "9F3D9739B11C2A4B08EA48512AC467F6",
|
||||||
"sha1": "B1D31D15A866EE0C3DCC9972FB92EE6FA2BE4D4A",
|
"sha1": "10E25C6EC9A30C731BF82E5DBA37BC693E9F615D",
|
||||||
"sha256": "E217F816094C5EE4CF366E6F168851C13B27D8CC9C42F392B637CAD1C20A8510",
|
"sha256": "5F8064636753265C7F1B1EE075DF77E1AE9BCE7E94831DE583784A0C13EB902F",
|
||||||
"xxh128": "F4A47B42150480BE1193EA7DA0DFD083"
|
"xxh128": "B92C6051418D532F7E933C08C44C4C88"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/cli.rs
Normal file
26
src/cli.rs
Normal 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
22
src/constants.rs
Normal 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
43
src/head_hash.rs
Normal 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())
|
||||||
|
}
|
||||||
98
src/main.rs
98
src/main.rs
@@ -1,3 +1,97 @@
|
|||||||
fn main() {
|
mod cli;
|
||||||
println!("Hello, world!");
|
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
123
src/meta/file.rs
Normal 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
5
src/meta/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod file;
|
||||||
|
mod tree;
|
||||||
|
|
||||||
|
pub use file::FileMeta;
|
||||||
|
pub use tree::DirSnapshot;
|
||||||
76
src/meta/tree.rs
Normal file
76
src/meta/tree.rs
Normal 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
68
src/utils.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user