style: Fix rustfmt formatting issues
This commit is contained in:
19
.github/workflows/rust.yml
vendored
19
.github/workflows/rust.yml
vendored
@@ -16,10 +16,25 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --verbose
|
run: cargo build --release
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --verbose
|
run: cargo test --release
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: cargo clippy -- -D warnings
|
run: cargo clippy -- -D warnings
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
|
|||||||
58
Cargo.lock
generated
58
Cargo.lock
generated
@@ -762,15 +762,6 @@ version = "0.8.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lock_api"
|
|
||||||
version = "0.4.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
|
||||||
dependencies = [
|
|
||||||
"scopeguard",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
@@ -898,29 +889,6 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot"
|
|
||||||
version = "0.12.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.9.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"smallvec",
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -978,15 +946,6 @@ version = "5.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -1119,12 +1078,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@@ -1229,15 +1182,6 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "signal-hook-registry"
|
|
||||||
version = "1.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.11"
|
version = "0.4.11"
|
||||||
@@ -1402,9 +1346,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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 = ["rt-multi-thread", "macros", "sync", "time"] }
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
22
src/cache.rs
22
src/cache.rs
@@ -46,8 +46,7 @@ impl Cache {
|
|||||||
pub fn load_or_create(cache_path: &Path) -> Self {
|
pub fn load_or_create(cache_path: &Path) -> Self {
|
||||||
if cache_path.exists() {
|
if cache_path.exists() {
|
||||||
match fs::read_to_string(cache_path) {
|
match fs::read_to_string(cache_path) {
|
||||||
Ok(content) => {
|
Ok(content) => match serde_json::from_str::<Cache>(&content) {
|
||||||
match serde_json::from_str::<Cache>(&content) {
|
|
||||||
Ok(cache) => {
|
Ok(cache) => {
|
||||||
println!("Loaded cache with {} entries", cache.entries.len());
|
println!("Loaded cache with {} entries", cache.entries.len());
|
||||||
cache
|
cache
|
||||||
@@ -56,8 +55,7 @@ impl Cache {
|
|||||||
println!("Cache corrupted, creating new cache");
|
println!("Cache corrupted, creating new cache");
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!("Failed to read cache, creating new cache");
|
println!("Failed to read cache, creating new cache");
|
||||||
Self::new()
|
Self::new()
|
||||||
@@ -79,7 +77,11 @@ impl Cache {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cached_response(&self, filenames: &[String], base_path: &Path) -> Option<OrganizationPlan> {
|
pub fn get_cached_response(
|
||||||
|
&self,
|
||||||
|
filenames: &[String],
|
||||||
|
base_path: &Path,
|
||||||
|
) -> Option<OrganizationPlan> {
|
||||||
let cache_key = self.generate_cache_key(filenames);
|
let cache_key = self.generate_cache_key(filenames);
|
||||||
|
|
||||||
if let Some(entry) = self.entries.get(&cache_key) {
|
if let Some(entry) = self.entries.get(&cache_key) {
|
||||||
@@ -163,10 +165,7 @@ impl Cache {
|
|||||||
|
|
||||||
fn get_file_metadata(file_path: &Path) -> Result<FileMetadata, Box<dyn std::error::Error>> {
|
fn get_file_metadata(file_path: &Path) -> Result<FileMetadata, Box<dyn std::error::Error>> {
|
||||||
let metadata = fs::metadata(file_path)?;
|
let metadata = fs::metadata(file_path)?;
|
||||||
let modified = metadata
|
let modified = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
|
||||||
.modified()?
|
|
||||||
.duration_since(UNIX_EPOCH)?
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
Ok(FileMetadata {
|
Ok(FileMetadata {
|
||||||
size: metadata.len(),
|
size: metadata.len(),
|
||||||
@@ -182,9 +181,8 @@ impl Cache {
|
|||||||
|
|
||||||
let initial_count = self.entries.len();
|
let initial_count = self.entries.len();
|
||||||
|
|
||||||
self.entries.retain(|_, entry| {
|
self.entries
|
||||||
current_time - entry.timestamp < max_age_seconds
|
.retain(|_, entry| current_time - entry.timestamp < max_age_seconds);
|
||||||
});
|
|
||||||
|
|
||||||
let removed_count = initial_count - self.entries.len();
|
let removed_count = initial_count - self.entries.len();
|
||||||
if removed_count > 0 {
|
if removed_count > 0 {
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ fn test_cache_response_file_changed() {
|
|||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
|
||||||
let mut file = File::create(&file_path).unwrap();
|
let mut file = File::create(&file_path).unwrap();
|
||||||
file.write_all(b"modified content longer than original").unwrap();
|
file.write_all(b"modified content longer than original")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let cached = cache.get_cached_response(&filenames, base_path);
|
let cached = cache.get_cached_response(&filenames, base_path);
|
||||||
assert!(cached.is_none());
|
assert!(cached.is_none());
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ impl Config {
|
|||||||
|
|
||||||
pub fn get_or_prompt_api_key() -> Result<String, Box<dyn std::error::Error>> {
|
pub fn get_or_prompt_api_key() -> Result<String, Box<dyn std::error::Error>> {
|
||||||
if let Ok(config) = Config::load()
|
if let Ok(config) = Config::load()
|
||||||
&& !config.api_key.is_empty() {
|
&& !config.api_key.is_empty()
|
||||||
|
{
|
||||||
return Ok(config.api_key);
|
return Ok(config.api_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +95,9 @@ pub fn get_or_prompt_api_key() -> Result<String, Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
pub fn get_or_prompt_download_folder() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
pub fn get_or_prompt_download_folder() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||||
if let Ok(config) = Config::load()
|
if let Ok(config) = Config::load()
|
||||||
&& !config.download_folder.as_os_str().is_empty() && config.download_folder.exists() {
|
&& !config.download_folder.as_os_str().is_empty()
|
||||||
|
&& config.download_folder.exists()
|
||||||
|
{
|
||||||
return Ok(config.download_folder);
|
return Ok(config.download_folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,30 +18,42 @@ fn test_config_serialization() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_api_key_valid() {
|
fn test_validate_api_key_valid() {
|
||||||
assert!(crate::prompt::Prompter::validate_api_key("AIzaSyB1234567890123456789012345678"));
|
assert!(crate::prompt::Prompter::validate_api_key(
|
||||||
assert!(crate::prompt::Prompter::validate_api_key("AIzaSyB123456789012345678901234567890"));
|
"AIzaSyB1234567890123456789012345678"
|
||||||
|
));
|
||||||
|
assert!(crate::prompt::Prompter::validate_api_key(
|
||||||
|
"AIzaSyB123456789012345678901234567890"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_api_key_invalid() {
|
fn test_validate_api_key_invalid() {
|
||||||
assert!(!crate::prompt::Prompter::validate_api_key(""));
|
assert!(!crate::prompt::Prompter::validate_api_key(""));
|
||||||
assert!(!crate::prompt::Prompter::validate_api_key("invalid_key"));
|
assert!(!crate::prompt::Prompter::validate_api_key("invalid_key"));
|
||||||
assert!(!crate::prompt::Prompter::validate_api_key("BizaSyB1234567890123456789012345678"));
|
assert!(!crate::prompt::Prompter::validate_api_key(
|
||||||
|
"BizaSyB1234567890123456789012345678"
|
||||||
|
));
|
||||||
assert!(!crate::prompt::Prompter::validate_api_key("short"));
|
assert!(!crate::prompt::Prompter::validate_api_key("short"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_folder_path_valid() {
|
fn test_validate_folder_path_valid() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
assert!(crate::prompt::Prompter::validate_folder_path(temp_dir.path()));
|
assert!(crate::prompt::Prompter::validate_folder_path(
|
||||||
|
temp_dir.path()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_folder_path_invalid() {
|
fn test_validate_folder_path_invalid() {
|
||||||
assert!(!crate::prompt::Prompter::validate_folder_path(Path::new("/nonexistent/path/that/does/not/exist")));
|
assert!(!crate::prompt::Prompter::validate_folder_path(Path::new(
|
||||||
|
"/nonexistent/path/that/does/not/exist"
|
||||||
|
)));
|
||||||
|
|
||||||
let temp_file = tempfile::NamedTempFile::new().unwrap();
|
let temp_file = tempfile::NamedTempFile::new().unwrap();
|
||||||
assert!(!crate::prompt::Prompter::validate_folder_path(temp_file.path()));
|
assert!(!crate::prompt::Prompter::validate_folder_path(
|
||||||
|
temp_file.path()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
61
src/files.rs
61
src/files.rs
@@ -39,7 +39,8 @@ impl FileBatch {
|
|||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_file()
|
if path.is_file()
|
||||||
&& let Ok(relative_path) = path.strip_prefix(&root_path) {
|
&& 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(path);
|
paths.push(path);
|
||||||
}
|
}
|
||||||
@@ -91,10 +92,7 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
|
|||||||
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();
|
||||||
if io::stdin()
|
if io::stdin().read_line(&mut input).is_err() {
|
||||||
.read_line(&mut input)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
eprintln!("\n{}", "Failed to read input. Operation cancelled.".red());
|
eprintln!("\n{}", "Failed to read input. Operation cancelled.".red());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,11 +142,7 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
|
|||||||
match move_file_cross_platform(&source, &target) {
|
match move_file_cross_platform(&source, &target) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
if item.sub_category.is_empty() {
|
if item.sub_category.is_empty() {
|
||||||
println!(
|
println!("Moved: {} -> {}/", item.filename, item.category.green());
|
||||||
"Moved: {} -> {}/",
|
|
||||||
item.filename,
|
|
||||||
item.category.green()
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"Moved: {} -> {}/{}",
|
"Moved: {} -> {}/{}",
|
||||||
@@ -187,50 +181,17 @@ pub fn execute_move(base_path: &Path, plan: OrganizationPlan) {
|
|||||||
moved_count.to_string().green(),
|
moved_count.to_string().green(),
|
||||||
error_count.to_string().red()
|
error_count.to_string().red()
|
||||||
);
|
);
|
||||||
} pub fn is_text_file(path: &Path) -> bool {
|
}
|
||||||
|
pub fn is_text_file(path: &Path) -> bool {
|
||||||
let text_extensions = [
|
let text_extensions = [
|
||||||
"txt",
|
"txt", "md", "rs", "py", "js", "ts", "jsx", "tsx", "html", "css", "json", "xml", "csv",
|
||||||
"md",
|
"yaml", "yml", "toml", "ini", "cfg", "conf", "log", "sh", "bat", "ps1", "sql", "c", "cpp",
|
||||||
"rs",
|
"h", "hpp", "java", "go", "rb", "php", "swift", "kt", "scala", "lua", "r", "m",
|
||||||
"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()
|
||||||
&& 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
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ fn test_read_file_sample_with_limit() {
|
|||||||
let file_path = temp_dir.path().join("test.txt");
|
let file_path = temp_dir.path().join("test.txt");
|
||||||
|
|
||||||
let mut file = File::create(&file_path).unwrap();
|
let mut file = File::create(&file_path).unwrap();
|
||||||
file.write_all(b"Hello, World! This is a long text.").unwrap();
|
file.write_all(b"Hello, World! This is a long text.")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let content = read_file_sample(&file_path, 5);
|
let content = read_file_sample(&file_path, 5);
|
||||||
assert_eq!(content, Some("Hello".to_string()));
|
assert_eq!(content, Some("Hello".to_string()));
|
||||||
@@ -92,13 +93,11 @@ fn test_read_file_sample_nonexistent() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_organization_plan_serialization() {
|
fn test_organization_plan_serialization() {
|
||||||
let plan = OrganizationPlan {
|
let plan = OrganizationPlan {
|
||||||
files: vec![
|
files: vec![FileCategory {
|
||||||
FileCategory {
|
|
||||||
filename: "test.txt".to_string(),
|
filename: "test.txt".to_string(),
|
||||||
category: "Documents".to_string(),
|
category: "Documents".to_string(),
|
||||||
sub_category: "Text".to_string(),
|
sub_category: "Text".to_string(),
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&plan).unwrap();
|
let json = serde_json::to_string(&plan).unwrap();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::cache::Cache;
|
use crate::cache::Cache;
|
||||||
|
use crate::files::OrganizationPlan;
|
||||||
use crate::gemini_errors::GeminiError;
|
use crate::gemini_errors::GeminiError;
|
||||||
use crate::gemini_helpers::PromptBuilder;
|
use crate::gemini_helpers::PromptBuilder;
|
||||||
use crate::gemini_types::{GeminiResponse, OrganizationPlanResponse};
|
use crate::gemini_types::{GeminiResponse, OrganizationPlanResponse};
|
||||||
use crate::files::OrganizationPlan;
|
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -71,7 +71,8 @@ impl GeminiClient {
|
|||||||
let url = self.build_url();
|
let url = self.build_url();
|
||||||
|
|
||||||
if let (Some(cache), Some(base_path)) = (cache.as_ref(), base_path)
|
if let (Some(cache), Some(base_path)) = (cache.as_ref(), base_path)
|
||||||
&& let Some(cached_response) = cache.get_cached_response(&filenames, base_path) {
|
&& let Some(cached_response) = cache.get_cached_response(&filenames, base_path)
|
||||||
|
{
|
||||||
return Ok(cached_response);
|
return Ok(cached_response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +108,8 @@ impl GeminiClient {
|
|||||||
return Err(GeminiError::from_response(res).await);
|
return Err(GeminiError::from_response(res).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
let gemini_response: GeminiResponse = res.json().await
|
let gemini_response: GeminiResponse =
|
||||||
.map_err(GeminiError::NetworkError)?;
|
res.json().await.map_err(GeminiError::NetworkError)?;
|
||||||
|
|
||||||
let raw_text = self.extract_text_from_response(&gemini_response)?;
|
let raw_text = self.extract_text_from_response(&gemini_response)?;
|
||||||
let plan_response: OrganizationPlanResponse = serde_json::from_str(&raw_text)?;
|
let plan_response: OrganizationPlanResponse = serde_json::from_str(&raw_text)?;
|
||||||
@@ -116,10 +117,7 @@ impl GeminiClient {
|
|||||||
Ok(plan_response.to_organization_plan())
|
Ok(plan_response.to_organization_plan())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_text_from_response(
|
fn extract_text_from_response(&self, response: &GeminiResponse) -> Result<String, GeminiError> {
|
||||||
&self,
|
|
||||||
response: &GeminiResponse,
|
|
||||||
) -> Result<String, GeminiError> {
|
|
||||||
response
|
response
|
||||||
.candidates
|
.candidates
|
||||||
.first()
|
.first()
|
||||||
@@ -244,7 +242,11 @@ impl GeminiClient {
|
|||||||
self.extract_subcategory_from_response(&gemini_response, filename)
|
self.extract_subcategory_from_response(&gemini_response, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_subcategory_from_response(&self, response: &GeminiResponse, _filename: &str) -> String {
|
fn extract_subcategory_from_response(
|
||||||
|
&self,
|
||||||
|
response: &GeminiResponse,
|
||||||
|
_filename: &str,
|
||||||
|
) -> String {
|
||||||
match self.extract_text_from_response(response) {
|
match self.extract_text_from_response(response) {
|
||||||
Ok(text) => {
|
Ok(text) => {
|
||||||
let sub_category = text.trim();
|
let sub_category = text.trim();
|
||||||
|
|||||||
@@ -98,19 +98,22 @@ impl GeminiError {
|
|||||||
"RESOURCE_EXHAUSTED" => {
|
"RESOURCE_EXHAUSTED" => {
|
||||||
if let Some(retry_info) = details.iter().find(|d| d.retry_delay.is_some())
|
if let Some(retry_info) = details.iter().find(|d| d.retry_delay.is_some())
|
||||||
&& let Some(retry_delay) = &retry_info.retry_delay
|
&& let Some(retry_delay) = &retry_info.retry_delay
|
||||||
&& let Ok(seconds) = retry_delay.parse::<u32>() {
|
&& let Ok(seconds) = retry_delay.parse::<u32>()
|
||||||
return GeminiError::RateLimitExceeded { retry_after: seconds };
|
{
|
||||||
|
return GeminiError::RateLimitExceeded {
|
||||||
|
retry_after: seconds,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(quota_info) = details.iter().find(|d| d.quota_limit.is_some()) {
|
if let Some(quota_info) = details.iter().find(|d| d.quota_limit.is_some()) {
|
||||||
let limit = quota_info.quota_limit.as_deref().unwrap_or("unknown");
|
let limit = quota_info.quota_limit.as_deref().unwrap_or("unknown");
|
||||||
return GeminiError::QuotaExceeded {
|
return GeminiError::QuotaExceeded {
|
||||||
limit: limit.to_string()
|
limit: limit.to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
GeminiError::QuotaExceeded {
|
GeminiError::QuotaExceeded {
|
||||||
limit: "usage limit".to_string()
|
limit: "usage limit".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"NOT_FOUND" => {
|
"NOT_FOUND" => {
|
||||||
@@ -118,69 +121,57 @@ impl GeminiError {
|
|||||||
let model = extract_model_name(&error_detail.message);
|
let model = extract_model_name(&error_detail.message);
|
||||||
GeminiError::ModelNotFound { model }
|
GeminiError::ModelNotFound { model }
|
||||||
}
|
}
|
||||||
"UNAUTHENTICATED" => {
|
"UNAUTHENTICATED" => GeminiError::InvalidApiKey,
|
||||||
GeminiError::InvalidApiKey
|
|
||||||
}
|
|
||||||
"PERMISSION_DENIED" => {
|
"PERMISSION_DENIED" => {
|
||||||
if error_detail.message.to_lowercase().contains("policy") {
|
if error_detail.message.to_lowercase().contains("policy") {
|
||||||
GeminiError::ContentPolicyViolation {
|
GeminiError::ContentPolicyViolation {
|
||||||
reason: error_detail.message
|
reason: error_detail.message,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GeminiError::InvalidRequest {
|
GeminiError::InvalidRequest {
|
||||||
details: error_detail.message
|
details: error_detail.message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"INVALID_ARGUMENT" => {
|
"INVALID_ARGUMENT" => GeminiError::InvalidRequest {
|
||||||
GeminiError::InvalidRequest {
|
details: error_detail.message,
|
||||||
details: error_detail.message
|
},
|
||||||
}
|
"UNAVAILABLE" => GeminiError::ServiceUnavailable {
|
||||||
}
|
reason: error_detail.message,
|
||||||
"UNAVAILABLE" => {
|
},
|
||||||
GeminiError::ServiceUnavailable {
|
"DEADLINE_EXCEEDED" => GeminiError::Timeout { seconds: 60 },
|
||||||
reason: error_detail.message
|
"INTERNAL" => GeminiError::InternalError {
|
||||||
}
|
details: error_detail.message,
|
||||||
}
|
},
|
||||||
"DEADLINE_EXCEEDED" => {
|
_ => GeminiError::ApiError {
|
||||||
GeminiError::Timeout { seconds: 60 }
|
|
||||||
}
|
|
||||||
"INTERNAL" => {
|
|
||||||
GeminiError::InternalError {
|
|
||||||
details: error_detail.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
GeminiError::ApiError {
|
|
||||||
status,
|
status,
|
||||||
message: error_detail.message
|
message: error_detail.message,
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_status_code(status: reqwest::StatusCode, error_text: &str) -> Self {
|
fn from_status_code(status: reqwest::StatusCode, error_text: &str) -> Self {
|
||||||
match status.as_u16() {
|
match status.as_u16() {
|
||||||
400 => GeminiError::InvalidRequest {
|
400 => GeminiError::InvalidRequest {
|
||||||
details: error_text.to_string()
|
details: error_text.to_string(),
|
||||||
},
|
},
|
||||||
401 => GeminiError::InvalidApiKey,
|
401 => GeminiError::InvalidApiKey,
|
||||||
403 => GeminiError::ContentPolicyViolation {
|
403 => GeminiError::ContentPolicyViolation {
|
||||||
reason: error_text.to_string()
|
reason: error_text.to_string(),
|
||||||
},
|
},
|
||||||
404 => GeminiError::ModelNotFound {
|
404 => GeminiError::ModelNotFound {
|
||||||
model: "unknown".to_string()
|
model: "unknown".to_string(),
|
||||||
},
|
},
|
||||||
429 => GeminiError::RateLimitExceeded { retry_after: 60 },
|
429 => GeminiError::RateLimitExceeded { retry_after: 60 },
|
||||||
500 => GeminiError::InternalError {
|
500 => GeminiError::InternalError {
|
||||||
details: error_text.to_string()
|
details: error_text.to_string(),
|
||||||
},
|
},
|
||||||
502..=504 => GeminiError::ServiceUnavailable {
|
502..=504 => GeminiError::ServiceUnavailable {
|
||||||
reason: error_text.to_string()
|
reason: error_text.to_string(),
|
||||||
},
|
},
|
||||||
_ => GeminiError::ApiError {
|
_ => GeminiError::ApiError {
|
||||||
status: status.as_u16(),
|
status: status.as_u16(),
|
||||||
message: error_text.to_string()
|
message: error_text.to_string(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +207,8 @@ fn extract_model_name(message: &str) -> String {
|
|||||||
// Try to extract model name from error message
|
// Try to extract model name from error message
|
||||||
// Example: "Model 'gemini-1.5-flash' not found"
|
// Example: "Model 'gemini-1.5-flash' not found"
|
||||||
if let Some(start) = message.find('\'')
|
if let Some(start) = message.find('\'')
|
||||||
&& let Some(end) = message[start + 1..].find('\'') {
|
&& let Some(end) = message[start + 1..].find('\'')
|
||||||
|
{
|
||||||
return message[start + 1..start + 1 + end].to_string();
|
return message[start + 1..start + 1 + end].to_string();
|
||||||
}
|
}
|
||||||
"unknown".to_string()
|
"unknown".to_string()
|
||||||
|
|||||||
81
src/main.rs
81
src/main.rs
@@ -60,7 +60,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", "Gemini Plan received! Performing deep inspection...".green());
|
println!(
|
||||||
|
"{}",
|
||||||
|
"Gemini Plan received! Performing deep inspection...".green()
|
||||||
|
);
|
||||||
|
|
||||||
let client = Arc::new(client);
|
let client = Arc::new(client);
|
||||||
let semaphore = Arc::new(tokio::sync::Semaphore::new(args.max_concurrent));
|
let semaphore = Arc::new(tokio::sync::Semaphore::new(args.max_concurrent));
|
||||||
@@ -81,7 +84,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let _permit = semaphore.acquire().await.unwrap();
|
let _permit = semaphore.acquire().await.unwrap();
|
||||||
if let Some(content) = noentropy::files::read_file_sample(&path, 5000) {
|
if let Some(content) = noentropy::files::read_file_sample(&path, 5000) {
|
||||||
println!("Reading content of {}...", filename.green());
|
println!("Reading content of {}...", filename.green());
|
||||||
client.get_ai_sub_category(&filename, &category, &content).await
|
client
|
||||||
|
.get_ai_sub_category(&filename, &category, &content)
|
||||||
|
.await
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
@@ -101,10 +106,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("{}", "Deep inspection complete! Moving Files.....".green());
|
println!("{}", "Deep inspection complete! Moving Files.....".green());
|
||||||
|
|
||||||
if args.dry_run {
|
if args.dry_run {
|
||||||
println!(
|
println!("{} Dry run mode - skipping file moves.", "INFO:".cyan());
|
||||||
"{} Dry run mode - skipping file moves.",
|
|
||||||
"INFO:".cyan()
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
execute_move(&download_path, plan);
|
execute_move(&download_path, plan);
|
||||||
}
|
}
|
||||||
@@ -122,56 +124,71 @@ fn handle_gemini_error(error: GeminiError) {
|
|||||||
|
|
||||||
match error {
|
match error {
|
||||||
GeminiError::RateLimitExceeded { retry_after } => {
|
GeminiError::RateLimitExceeded { retry_after } => {
|
||||||
println!("{} API rate limit exceeded. Please wait {} seconds before trying again.",
|
println!(
|
||||||
"ERROR:".red(), retry_after);
|
"{} API rate limit exceeded. Please wait {} seconds before trying again.",
|
||||||
|
"ERROR:".red(),
|
||||||
|
retry_after
|
||||||
|
);
|
||||||
}
|
}
|
||||||
GeminiError::QuotaExceeded { limit } => {
|
GeminiError::QuotaExceeded { limit } => {
|
||||||
println!("{} Quota exceeded: {}. Please check your Gemini API usage.",
|
println!(
|
||||||
"ERROR:".red(), limit);
|
"{} Quota exceeded: {}. Please check your Gemini API usage.",
|
||||||
|
"ERROR:".red(),
|
||||||
|
limit
|
||||||
|
);
|
||||||
}
|
}
|
||||||
GeminiError::ModelNotFound { model } => {
|
GeminiError::ModelNotFound { model } => {
|
||||||
println!("{} Model '{}' not found. Please check the model name in the configuration.",
|
println!(
|
||||||
"ERROR:".red(), model);
|
"{} Model '{}' not found. Please check the model name in the configuration.",
|
||||||
|
"ERROR:".red(),
|
||||||
|
model
|
||||||
|
);
|
||||||
}
|
}
|
||||||
GeminiError::InvalidApiKey => {
|
GeminiError::InvalidApiKey => {
|
||||||
println!("{} Invalid API key. Please check your GEMINI_API_KEY environment variable.",
|
println!(
|
||||||
"ERROR:".red());
|
"{} Invalid API key. Please check your GEMINI_API_KEY environment variable.",
|
||||||
|
"ERROR:".red()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
GeminiError::ContentPolicyViolation { reason } => {
|
GeminiError::ContentPolicyViolation { reason } => {
|
||||||
println!("{} Content policy violation: {}",
|
println!("{} Content policy violation: {}", "ERROR:".red(), reason);
|
||||||
"ERROR:".red(), reason);
|
|
||||||
}
|
}
|
||||||
GeminiError::ServiceUnavailable { reason } => {
|
GeminiError::ServiceUnavailable { reason } => {
|
||||||
println!("{} Gemini service is temporarily unavailable: {}",
|
println!(
|
||||||
"ERROR:".red(), reason);
|
"{} Gemini service is temporarily unavailable: {}",
|
||||||
|
"ERROR:".red(),
|
||||||
|
reason
|
||||||
|
);
|
||||||
}
|
}
|
||||||
GeminiError::NetworkError(e) => {
|
GeminiError::NetworkError(e) => {
|
||||||
println!("{} Network error: {}",
|
println!("{} Network error: {}", "ERROR:".red(), e);
|
||||||
"ERROR:".red(), e);
|
|
||||||
}
|
}
|
||||||
GeminiError::Timeout { seconds } => {
|
GeminiError::Timeout { seconds } => {
|
||||||
println!("{} Request timed out after {} seconds.",
|
println!(
|
||||||
"ERROR:".red(), seconds);
|
"{} Request timed out after {} seconds.",
|
||||||
|
"ERROR:".red(),
|
||||||
|
seconds
|
||||||
|
);
|
||||||
}
|
}
|
||||||
GeminiError::InvalidRequest { details } => {
|
GeminiError::InvalidRequest { details } => {
|
||||||
println!("{} Invalid request: {}",
|
println!("{} Invalid request: {}", "ERROR:".red(), details);
|
||||||
"ERROR:".red(), details);
|
|
||||||
}
|
}
|
||||||
GeminiError::ApiError { status, message } => {
|
GeminiError::ApiError { status, message } => {
|
||||||
println!("{} API error (HTTP {}): {}",
|
println!(
|
||||||
"ERROR:".red(), status, message);
|
"{} API error (HTTP {}): {}",
|
||||||
|
"ERROR:".red(),
|
||||||
|
status,
|
||||||
|
message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
GeminiError::InvalidResponse(msg) => {
|
GeminiError::InvalidResponse(msg) => {
|
||||||
println!("{} Invalid response from Gemini: {}",
|
println!("{} Invalid response from Gemini: {}", "ERROR:".red(), msg);
|
||||||
"ERROR:".red(), msg);
|
|
||||||
}
|
}
|
||||||
GeminiError::InternalError { details } => {
|
GeminiError::InternalError { details } => {
|
||||||
println!("{} Internal server error: {}",
|
println!("{} Internal server error: {}", "ERROR:".red(), details);
|
||||||
"ERROR:".red(), details);
|
|
||||||
}
|
}
|
||||||
GeminiError::SerializationError(e) => {
|
GeminiError::SerializationError(e) => {
|
||||||
println!("{} JSON serialization error: {}",
|
println!("{} JSON serialization error: {}", "ERROR:".red(), e);
|
||||||
"ERROR:".red(), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ pub struct Prompter;
|
|||||||
impl Prompter {
|
impl Prompter {
|
||||||
pub fn prompt_api_key() -> Result<String, Box<dyn std::error::Error>> {
|
pub fn prompt_api_key() -> Result<String, Box<dyn std::error::Error>> {
|
||||||
println!();
|
println!();
|
||||||
println!("Get your API key at: {}", "https://ai.google.dev/".cyan().underline());
|
println!(
|
||||||
|
"Get your API key at: {}",
|
||||||
|
"https://ai.google.dev/".cyan().underline()
|
||||||
|
);
|
||||||
println!("Enter your API Key (starts with 'AIza'):");
|
println!("Enter your API Key (starts with 'AIza'):");
|
||||||
|
|
||||||
let mut attempts = 0;
|
let mut attempts = 0;
|
||||||
@@ -29,7 +32,10 @@ impl Prompter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
Self::print_validation_error("Invalid API key format. Must start with 'AIza' and be around 39 characters.", attempts);
|
Self::print_validation_error(
|
||||||
|
"Invalid API key format. Must start with 'AIza' and be around 39 characters.",
|
||||||
|
attempts,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err("Max retries exceeded. Please run again with a valid API key.".into())
|
Err("Max retries exceeded. Please run again with a valid API key.".into())
|
||||||
@@ -102,10 +108,7 @@ impl Prompter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_api_key(key: &str) -> bool {
|
pub fn validate_api_key(key: &str) -> bool {
|
||||||
!key.is_empty()
|
!key.is_empty() && key.starts_with("AIza") && key.len() >= 35 && key.len() <= 50
|
||||||
&& key.starts_with("AIza")
|
|
||||||
&& key.len() >= 35
|
|
||||||
&& key.len() <= 50
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_folder_path(path: &Path) -> bool {
|
pub fn validate_folder_path(path: &Path) -> bool {
|
||||||
@@ -120,7 +123,8 @@ impl Prompter {
|
|||||||
|
|
||||||
pub fn expand_home(path: &str) -> String {
|
pub fn expand_home(path: &str) -> String {
|
||||||
if path.starts_with("~/")
|
if path.starts_with("~/")
|
||||||
&& let Some(base_dirs) = BaseDirs::new() {
|
&& let Some(base_dirs) = BaseDirs::new()
|
||||||
|
{
|
||||||
let home = base_dirs.home_dir();
|
let home = base_dirs.home_dir();
|
||||||
return path.replacen("~", &home.to_string_lossy(), 1);
|
return path.replacen("~", &home.to_string_lossy(), 1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user