fix: Add Windows compatibility for file operations

- Fix home directory detection using BaseDirs from directories crate
- Add cross-platform file moving with copy+delete fallback
- Replace Unix-specific HOME env var usage
- Handle cross-filesystem moves on Windows automatically
This commit is contained in:
2025-12-28 23:54:53 +05:30
parent 5a92ecdeb7
commit 868ef57498
4 changed files with 716 additions and 102 deletions

372
Cargo.lock generated
View File

@@ -2,6 +2,56 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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 0.61.2",
]
[[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 0.61.2",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -57,6 +107,52 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 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]] [[package]]
name = "colored" name = "colored"
version = "3.0.0" version = "3.0.0"
@@ -111,6 +207,27 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "directories"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@@ -122,18 +239,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -348,6 +453,12 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@@ -602,13 +713,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "itertools" name = "is_terminal_polyfill"
version = "0.14.0" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
@@ -632,6 +740,16 @@ version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libredox"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
"bitflags",
"libc",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.11.0" version = "0.11.0"
@@ -703,18 +821,18 @@ dependencies = [
name = "noentropy" name = "noentropy"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap",
"colored", "colored",
"dotenv", "directories",
"futures", "futures",
"hex", "hex",
"itertools",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"thiserror", "thiserror 2.0.17",
"tokio", "tokio",
"walkdir", "toml",
] ]
[[package]] [[package]]
@@ -723,6 +841,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.75" version = "0.10.75"
@@ -767,6 +891,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@@ -856,6 +986,17 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.26" version = "0.12.26"
@@ -968,15 +1109,6 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.28" version = "0.1.28"
@@ -1058,6 +1190,15 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@@ -1124,6 +1265,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@@ -1195,13 +1342,33 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.17" version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 2.0.17",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@@ -1286,6 +1453,47 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@@ -1392,6 +1600,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
@@ -1404,16 +1618,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@@ -1506,15 +1710,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
@@ -1550,6 +1745,15 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@@ -1577,6 +1781,21 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@@ -1610,6 +1829,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.1", "windows_x86_64_msvc 0.53.1",
] ]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -1622,6 +1847,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -1634,6 +1865,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@@ -1658,6 +1895,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@@ -1670,6 +1913,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@@ -1682,6 +1931,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -1694,6 +1949,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -1706,6 +1967,15 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.46.0" version = "0.46.0"

View File

@@ -4,15 +4,15 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
clap = { version = "4.5.23", features = ["derive"] }
colored = "3.0.0" colored = "3.0.0"
dotenv = "0.15.0" directories = "5.0.1"
futures = "0.3.31" futures = "0.3.31"
hex = "0.4.3" hex = "0.4.3"
itertools = "0.14.0"
reqwest = { version = "0.12.26", features = ["json"] } reqwest = { version = "0.12.26", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"
sha2 = "0.10.8" sha2 = "0.10.8"
thiserror = "2.0.11" thiserror = "2.0.11"
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }
walkdir = "2.5.0" toml = "0.8.19"

271
src/config.rs Normal file
View File

@@ -0,0 +1,271 @@
use colored::*;
use directories::{BaseDirs, ProjectDirs};
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
const MAX_RETRIES: u32 = 3;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub api_key: String,
pub download_folder: PathBuf,
}
impl Config {
fn get_config_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
if let Some(proj_dirs) = ProjectDirs::from("dev", "noentropy", "NoEntropy") {
let config_dir = proj_dirs.config_dir().to_path_buf();
fs::create_dir_all(&config_dir)?;
Ok(config_dir)
} else {
Err("Failed to determine config directory".into())
}
}
fn get_config_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
Ok(Self::get_config_dir()?.join("config.toml"))
}
pub fn load() -> Result<Config, Box<dyn std::error::Error>> {
let config_path = Self::get_config_path()?;
if !config_path.exists() {
return Err("Config file not found".into());
}
let content = fs::read_to_string(&config_path)?;
let config: Config = toml::from_str(&content)?;
if config.api_key.is_empty() {
return Err("API key not found in config file".into());
}
Ok(config)
}
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let config_path = Self::get_config_path()?;
let toml_string = toml::to_string_pretty(self)?;
fs::write(&config_path, toml_string)?;
println!(
"{} Configuration saved to {}",
"".green(),
config_path.display().to_string().yellow()
);
Ok(())
}
pub fn get_api_key() -> Result<String, Box<dyn std::error::Error>> {
match Self::load() {
Ok(config) => Ok(config.api_key),
Err(_) => Err("API key not configured".into()),
}
}
pub fn get_download_folder() -> Result<PathBuf, Box<dyn std::error::Error>> {
match Self::load() {
Ok(config) => Ok(config.download_folder),
Err(_) => Err("Download folder not configured".into()),
}
}
}
pub fn get_or_prompt_api_key() -> Result<String, Box<dyn std::error::Error>> {
if let Ok(config) = Config::load() {
if !config.api_key.is_empty() {
return Ok(config.api_key);
}
}
println!();
println!("{}", "🔑 NoEntropy Configuration".bold().cyan());
println!("{}", "─────────────────────────────".cyan());
let api_key = prompt_api_key()?;
let mut config = if let Ok(cfg) = Config::load() {
cfg
} else {
Config {
api_key: api_key.clone(),
download_folder: PathBuf::new(),
}
};
config.api_key = api_key.clone();
config.save()?;
println!();
Ok(api_key)
}
pub fn get_or_prompt_download_folder() -> Result<PathBuf, Box<dyn std::error::Error>> {
if let Ok(config) = Config::load() {
if !config.download_folder.as_os_str().is_empty() && config.download_folder.exists() {
return Ok(config.download_folder);
}
}
println!();
println!("{}", "📁 Download folder not configured.".yellow());
let folder_path = prompt_download_folder()?;
let mut config = if let Ok(cfg) = Config::load() {
cfg
} else {
Config {
api_key: String::new(),
download_folder: folder_path.clone(),
}
};
config.download_folder = folder_path.clone();
config.save()?;
println!();
Ok(folder_path)
}
fn prompt_api_key() -> Result<String, Box<dyn std::error::Error>> {
let mut attempts = 0;
println!();
println!("Get your API key at: {}", "https://ai.google.dev/".cyan().underline());
println!("Enter your API Key (starts with 'AIza'):");
while attempts < MAX_RETRIES {
print!("API Key: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let key = input.trim();
if validate_api_key(key) {
return Ok(key.to_string());
}
attempts += 1;
let remaining = MAX_RETRIES - attempts;
eprintln!(
"{} Invalid API key format. Must start with 'AIza' and be around 39 characters.",
"".red()
);
if remaining > 0 {
eprintln!("Try again ({} attempts remaining):", remaining);
}
}
Err("Max retries exceeded. Please run again with a valid API key.".into())
}
fn prompt_download_folder() -> Result<PathBuf, Box<dyn std::error::Error>> {
let default_path = get_default_downloads_folder();
let default_display = default_path.to_string_lossy();
let mut attempts = 0;
println!(
"Enter path to folder to organize (e.g., {}):",
default_display.yellow()
);
println!("Or press Enter to use default: {}", default_display.green());
println!("Folder path: ");
while attempts < MAX_RETRIES {
print!("> ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
let path = if input.is_empty() {
default_path.clone()
} else {
let expanded = expand_home(input);
PathBuf::from(expanded)
};
if validate_folder_path(&path) {
return Ok(path);
}
attempts += 1;
let remaining = MAX_RETRIES - attempts;
eprintln!("{} Invalid folder path.", "".red());
if !path.exists() {
eprintln!(" Path does not exist: {}", path.display());
} else if !path.is_dir() {
eprintln!(" Path is not a directory: {}", path.display());
}
if remaining > 0 {
eprintln!("Try again ({} attempts remaining):", remaining);
println!("Folder path: ");
}
}
Err("Max retries exceeded. Please run again with a valid folder path.".into())
}
fn validate_api_key(key: &str) -> bool {
if key.is_empty() {
return false;
}
if !key.starts_with("AIza") {
return false;
}
if key.len() < 35 || key.len() > 50 {
return false;
}
true
}
fn validate_folder_path(path: &Path) -> bool {
if !path.exists() {
return false;
}
if !path.is_dir() {
return false;
}
true
}
fn get_default_downloads_folder() -> PathBuf {
if let Some(base_dirs) = BaseDirs::new() {
let home = base_dirs.home_dir();
return home.join("Downloads");
}
PathBuf::from("./Downloads")
}
fn expand_home(path: &str) -> String {
if path.starts_with("~/")
&& let Some(base_dirs) = BaseDirs::new()
{
let home = base_dirs.home_dir();
return path.replacen("~", &home.to_string_lossy(), 1);
}
path.to_string()
}

View File

@@ -2,7 +2,6 @@ use colored::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io; use std::io;
use std::{ffi::OsStr, fs, path::Path, path::PathBuf}; use std::{ffi::OsStr, fs, path::Path, path::PathBuf};
use walkdir::WalkDir;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FileCategory { pub struct FileCategory {
@@ -25,28 +24,54 @@ impl FileBatch {
pub fn from_path(root_path: PathBuf) -> Self { pub fn from_path(root_path: PathBuf) -> Self {
let mut filenames = Vec::new(); let mut filenames = Vec::new();
let mut paths = Vec::new(); let mut paths = Vec::new();
for entry in WalkDir::new(&root_path)
.into_iter() let entries = match fs::read_dir(&root_path) {
.filter_map(|e| e.ok()) Ok(entries) => entries,
.filter(|e| e.path().is_file()) Err(e) => {
{ eprintln!("Error reading directory {:?}: {}", root_path, e);
if let Ok(relative_path) = entry.path().strip_prefix(&root_path) { return FileBatch {
filenames: Vec::new(),
paths: Vec::new(),
};
}
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& let Ok(relative_path) = path.strip_prefix(&root_path) {
filenames.push(relative_path.to_string_lossy().into_owned()); filenames.push(relative_path.to_string_lossy().into_owned());
paths.push(entry.path().to_path_buf()); paths.push(path);
} }
} }
FileBatch { filenames, paths } FileBatch { filenames, paths }
} }
/// Helper to get the number of files found /// Helper to get the number of files found
pub fn count(&self) -> usize { pub fn count(&self) -> usize {
self.filenames.len() self.filenames.len()
} }
} }
/// Move a file with cross-platform compatibility
/// Tries rename first (fastest), falls back to copy+delete if needed (e.g., cross-filesystem on Windows)
fn move_file_cross_platform(source: &Path, target: &Path) -> io::Result<()> {
match fs::rename(source, target) {
Ok(()) => Ok(()),
Err(e) => {
if cfg!(windows) || e.kind() == io::ErrorKind::CrossesDevices {
fs::copy(source, target)?;
fs::remove_file(source)?;
Ok(())
} else {
Err(e)
}
}
}
}
pub fn execute_move(base_path: &Path, plan: OrganizationPlan) { pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
// ---------------------------------------------------------
// PHASE 1: PREVIEW (Show the plan)
// ---------------------------------------------------------
println!("\n{}", "--- EXECUTION PLAN ---".bold().underline()); println!("\n{}", "--- EXECUTION PLAN ---".bold().underline());
if plan.files.is_empty() { if plan.files.is_empty() {
@@ -54,7 +79,6 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
return; return;
} }
// Iterate by reference (&) so we don't consume the data yet
for item in &plan.files { for item in &plan.files {
let mut target_display = format!("{}", item.category.green()); let mut target_display = format!("{}", item.category.green());
if !item.sub_category.is_empty() { if !item.sub_category.is_empty() {
@@ -64,9 +88,6 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
println!("Plan: {} -> {}/", item.filename, target_display); println!("Plan: {} -> {}/", item.filename, target_display);
} }
// ---------------------------------------------------------
// PHASE 2: PROMPT (Ask for permission)
// ---------------------------------------------------------
eprint!("\nDo you want to apply these changes? [y/N]: "); eprint!("\nDo you want to apply these changes? [y/N]: ");
let mut input = String::new(); let mut input = String::new();
@@ -74,27 +95,25 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
.read_line(&mut input) .read_line(&mut input)
.is_err() .is_err()
{ {
println!("\n{}", "Failed to read input. Operation cancelled.".red()); eprintln!("\n{}", "Failed to read input. Operation cancelled.".red());
return; return;
} }
let input = input.trim().to_lowercase(); let input = input.trim().to_lowercase();
// If input is not "y" or "yes", abort.
if input != "y" && input != "yes" { if input != "y" && input != "yes" {
println!("\n{}", "Operation cancelled.".red()); println!("\n{}", "Operation cancelled.".red());
return; return;
} }
// ---------------------------------------------------------
// PHASE 3: EXECUTION (Actually move files)
// ---------------------------------------------------------
println!("\n{}", "--- MOVING FILES ---".bold().underline()); println!("\n{}", "--- MOVING FILES ---".bold().underline());
let mut moved_count = 0;
let mut error_count = 0;
for item in plan.files { for item in plan.files {
let source = base_path.join(&item.filename); let source = base_path.join(&item.filename);
// Logic: Destination / Parent Category / Sub Category
let mut final_path = base_path.join(&item.category); let mut final_path = base_path.join(&item.category);
if !item.sub_category.is_empty() { if !item.sub_category.is_empty() {
@@ -109,25 +128,27 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
let target = final_path.join(&file_name); let target = final_path.join(&file_name);
// 1. Create the category/sub-category folder
// (Only need to call this once per file path)
if let Err(e) = fs::create_dir_all(&final_path) { if let Err(e) = fs::create_dir_all(&final_path) {
println!( eprintln!(
"{} Failed to create dir {:?}: {}", "{} Failed to create dir {:?}: {}",
"ERROR:".red(), "ERROR:".red(),
final_path, final_path,
e e
); );
continue; // Skip moving this file if we can't make the folder error_count += 1;
continue;
} }
// 2. Move the file if let Ok(metadata) = fs::metadata(&source) {
if source.exists() { if metadata.is_file() {
match fs::rename(&source, &target) { match move_file_cross_platform(&source, &target) {
Ok(_) => { Ok(_) => {
// Formatting the success message
if item.sub_category.is_empty() { if item.sub_category.is_empty() {
println!("Moved: {} -> {}/", item.filename, item.category.green()); println!(
"Moved: {} -> {}/",
item.filename,
item.category.green()
);
} else { } else {
println!( println!(
"Moved: {} -> {}/{}", "Moved: {} -> {}/{}",
@@ -136,30 +157,82 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
item.sub_category.blue() item.sub_category.blue()
); );
} }
moved_count += 1;
}
Err(e) => {
eprintln!("{} Failed to move {}: {}", "ERROR:".red(), item.filename, e);
error_count += 1;
} }
Err(e) => println!("{} Failed to move {}: {}", "ERROR:".red(), item.filename, e),
} }
} else { } else {
println!( eprintln!(
"{} Skipping {}: File not found", "{} Skipping {}: Not a file",
"WARN:".yellow(), "WARN:".yellow(),
item.filename item.filename
); );
} }
} else {
eprintln!(
"{} Skipping {}: File not found",
"WARN:".yellow(),
item.filename
);
error_count += 1;
}
} }
println!("\n{}", "Organization Complete!".bold().green()); println!("\n{}", "Organization Complete!".bold().green());
} // --- 1. Helper to check if file is likely text --- println!(
pub fn is_text_file(path: &Path) -> bool { "Files moved: {}, Errors: {}",
moved_count.to_string().green(),
error_count.to_string().red()
);
} pub fn is_text_file(path: &Path) -> bool {
let text_extensions = [ let text_extensions = [
"txt", "md", "rs", "py", "js", "html", "css", "json", "xml", "csv", "txt",
"md",
"rs",
"py",
"js",
"ts",
"jsx",
"tsx",
"html",
"css",
"json",
"xml",
"csv",
"yaml",
"yml",
"toml",
"ini",
"cfg",
"conf",
"log",
"sh",
"bat",
"ps1",
"sql",
"c",
"cpp",
"h",
"hpp",
"java",
"go",
"rb",
"php",
"swift",
"kt",
"scala",
"lua",
"r",
"m",
]; ];
if let Some(ext) = path.extension() { if let Some(ext) = path.extension()
if let Some(ext_str) = ext.to_str() { && let Some(ext_str) = ext.to_str() {
return text_extensions.contains(&ext_str.to_lowercase().as_str()); return text_extensions.contains(&ext_str.to_lowercase().as_str());
} }
}
false false
} }