diff --git a/Cargo.lock b/Cargo.lock index ecab8f5..237a0e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,56 @@ # It is not intended for manual editing. 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]] name = "atomic-waker" version = "1.1.2" @@ -57,6 +107,52 @@ 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 = "colored" version = "3.0.0" @@ -111,6 +207,27 @@ dependencies = [ "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]] name = "displaydoc" version = "0.2.5" @@ -122,18 +239,6 @@ dependencies = [ "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]] name = "encoding_rs" version = "0.8.35" @@ -348,6 +453,12 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -602,13 +713,10 @@ dependencies = [ ] [[package]] -name = "itertools" -version = "0.14.0" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" @@ -632,6 +740,16 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "linux-raw-sys" version = "0.11.0" @@ -703,18 +821,18 @@ dependencies = [ name = "noentropy" version = "0.1.0" dependencies = [ + "clap", "colored", - "dotenv", + "directories", "futures", "hex", - "itertools", "reqwest", "serde", "serde_json", "sha2", - "thiserror", + "thiserror 2.0.17", "tokio", - "walkdir", + "toml", ] [[package]] @@ -723,6 +841,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl" version = "0.10.75" @@ -767,6 +891,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.5" @@ -856,6 +986,17 @@ dependencies = [ "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]] name = "reqwest" version = "0.12.26" @@ -968,15 +1109,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "schannel" version = "0.1.28" @@ -1058,6 +1190,15 @@ dependencies = [ "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]] name = "serde_urlencoded" version = "0.7.1" @@ -1124,6 +1265,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1195,13 +1342,33 @@ dependencies = [ "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]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 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]] @@ -1286,6 +1453,47 @@ dependencies = [ "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]] name = "tower" version = "0.5.2" @@ -1392,6 +1600,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1404,16 +1618,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "want" version = "0.3.1" @@ -1506,15 +1710,6 @@ dependencies = [ "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]] name = "windows-link" version = "0.2.1" @@ -1550,6 +1745,15 @@ dependencies = [ "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]] name = "windows-sys" version = "0.52.0" @@ -1577,6 +1781,21 @@ dependencies = [ "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]] name = "windows-targets" version = "0.52.6" @@ -1610,6 +1829,12 @@ dependencies = [ "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]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1622,6 +1847,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -1634,6 +1865,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1658,6 +1895,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -1670,6 +1913,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1682,6 +1931,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -1694,6 +1949,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1706,6 +1967,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index a5536b5..9eeaf65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,15 +4,15 @@ version = "0.1.0" edition = "2024" [dependencies] +clap = { version = "4.5.23", features = ["derive"] } colored = "3.0.0" -dotenv = "0.15.0" +directories = "5.0.1" futures = "0.3.31" hex = "0.4.3" -itertools = "0.14.0" reqwest = { version = "0.12.26", features = ["json"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" sha2 = "0.10.8" thiserror = "2.0.11" tokio = { version = "1.48.0", features = ["full"] } -walkdir = "2.5.0" +toml = "0.8.19" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..7af6409 --- /dev/null +++ b/src/config.rs @@ -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> { + 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> { + Ok(Self::get_config_dir()?.join("config.toml")) + } + + pub fn load() -> Result> { + 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> { + 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> { + match Self::load() { + Ok(config) => Ok(config.api_key), + Err(_) => Err("API key not configured".into()), + } + } + + pub fn get_download_folder() -> Result> { + match Self::load() { + Ok(config) => Ok(config.download_folder), + Err(_) => Err("Download folder not configured".into()), + } + } +} + +pub fn get_or_prompt_api_key() -> Result> { + 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> { + 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> { + 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> { + 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() +} diff --git a/src/files.rs b/src/files.rs index e9b7ed0..776a5dc 100644 --- a/src/files.rs +++ b/src/files.rs @@ -2,7 +2,6 @@ use colored::*; use serde::{Deserialize, Serialize}; use std::io; use std::{ffi::OsStr, fs, path::Path, path::PathBuf}; -use walkdir::WalkDir; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FileCategory { @@ -25,28 +24,54 @@ impl FileBatch { pub fn from_path(root_path: PathBuf) -> Self { let mut filenames = Vec::new(); let mut paths = Vec::new(); - for entry in WalkDir::new(&root_path) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.path().is_file()) - { - if let Ok(relative_path) = entry.path().strip_prefix(&root_path) { - filenames.push(relative_path.to_string_lossy().into_owned()); - paths.push(entry.path().to_path_buf()); + + let entries = match fs::read_dir(&root_path) { + Ok(entries) => entries, + Err(e) => { + eprintln!("Error reading directory {:?}: {}", root_path, e); + 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()); + paths.push(path); + } } + FileBatch { filenames, paths } } + /// Helper to get the number of files found pub fn count(&self) -> usize { 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) { - // --------------------------------------------------------- - // PHASE 1: PREVIEW (Show the plan) - // --------------------------------------------------------- println!("\n{}", "--- EXECUTION PLAN ---".bold().underline()); if plan.files.is_empty() { @@ -54,7 +79,6 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) { return; } - // Iterate by reference (&) so we don't consume the data yet for item in &plan.files { let mut target_display = format!("{}", item.category.green()); 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); } - // --------------------------------------------------------- - // PHASE 2: PROMPT (Ask for permission) - // --------------------------------------------------------- eprint!("\nDo you want to apply these changes? [y/N]: "); let mut input = String::new(); @@ -74,27 +95,25 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) { .read_line(&mut input) .is_err() { - println!("\n{}", "Failed to read input. Operation cancelled.".red()); + eprintln!("\n{}", "Failed to read input. Operation cancelled.".red()); return; } let input = input.trim().to_lowercase(); - // If input is not "y" or "yes", abort. if input != "y" && input != "yes" { println!("\n{}", "Operation cancelled.".red()); return; } - // --------------------------------------------------------- - // PHASE 3: EXECUTION (Actually move files) - // --------------------------------------------------------- println!("\n{}", "--- MOVING FILES ---".bold().underline()); + let mut moved_count = 0; + let mut error_count = 0; + for item in plan.files { let source = base_path.join(&item.filename); - // Logic: Destination / Parent Category / Sub Category let mut final_path = base_path.join(&item.category); if !item.sub_category.is_empty() { @@ -109,57 +128,111 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) { 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) { - println!( + eprintln!( "{} Failed to create dir {:?}: {}", "ERROR:".red(), final_path, e ); - continue; // Skip moving this file if we can't make the folder + error_count += 1; + continue; } - // 2. Move the file - if source.exists() { - match fs::rename(&source, &target) { - Ok(_) => { - // Formatting the success message - if item.sub_category.is_empty() { - println!("Moved: {} -> {}/", item.filename, item.category.green()); - } else { - println!( - "Moved: {} -> {}/{}", - item.filename, - item.category.green(), - item.sub_category.blue() - ); + if let Ok(metadata) = fs::metadata(&source) { + if metadata.is_file() { + match move_file_cross_platform(&source, &target) { + Ok(_) => { + if item.sub_category.is_empty() { + println!( + "Moved: {} -> {}/", + item.filename, + item.category.green() + ); + } else { + println!( + "Moved: {} -> {}/{}", + item.filename, + item.category.green(), + 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 { + eprintln!( + "{} Skipping {}: Not a file", + "WARN:".yellow(), + item.filename + ); } } else { - println!( + eprintln!( "{} Skipping {}: File not found", "WARN:".yellow(), item.filename ); + error_count += 1; } } println!("\n{}", "Organization Complete!".bold().green()); -} // --- 1. Helper to check if file is likely text --- -pub fn is_text_file(path: &Path) -> bool { + println!( + "Files moved: {}, Errors: {}", + moved_count.to_string().green(), + error_count.to_string().red() + ); +} pub fn is_text_file(path: &Path) -> bool { 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_str) = ext.to_str() { + if let Some(ext) = path.extension() + && let Some(ext_str) = ext.to_str() { return text_extensions.contains(&ext_str.to_lowercase().as_str()); } - } false }