style: Fix rustfmt formatting issues

This commit is contained in:
2025-12-29 00:54:29 +05:30
parent d25dcaa0de
commit 06eff6fcf8
13 changed files with 214 additions and 268 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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]

View File

@@ -46,18 +46,16 @@ 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
}
Err(_) => {
println!("Cache corrupted, creating new cache");
Self::new()
}
} }
} Err(_) => {
println!("Cache corrupted, creating new cache");
Self::new()
}
},
Err(_) => { Err(_) => {
println!("Failed to read cache, creating new cache"); println!("Failed to read cache, creating new cache");
Self::new() Self::new()
@@ -73,13 +71,17 @@ impl Cache {
if let Some(parent) = cache_path.parent() { if let Some(parent) = cache_path.parent() {
fs::create_dir_all(parent)?; fs::create_dir_all(parent)?;
} }
let content = serde_json::to_string_pretty(self)?; let content = serde_json::to_string_pretty(self)?;
fs::write(cache_path, content)?; fs::write(cache_path, content)?;
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 {

View File

@@ -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());

View File

@@ -74,8 +74,9 @@ 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);
} }
println!(); println!();
@@ -94,8 +95,10 @@ 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()
return Ok(config.download_folder); && config.download_folder.exists()
{
return Ok(config.download_folder);
} }
println!(); println!();

View File

@@ -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]

View File

@@ -39,10 +39,11 @@ 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()); {
paths.push(path); filenames.push(relative_path.to_string_lossy().into_owned());
} paths.push(path);
}
} }
FileBatch { filenames, paths } FileBatch { filenames, paths }
@@ -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,52 +181,19 @@ 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
} }

View File

@@ -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();

View File

@@ -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,8 +71,9 @@ 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);
} }
let prompt = PromptBuilder::new(filenames.clone()).build_categorization_prompt(); let prompt = PromptBuilder::new(filenames.clone()).build_categorization_prompt();
@@ -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();

View File

@@ -93,24 +93,27 @@ impl GeminiError {
fn from_gemini_error(error_detail: GeminiErrorDetail, status: u16) -> Self { fn from_gemini_error(error_detail: GeminiErrorDetail, status: u16) -> Self {
let details = error_detail.details; let details = error_detail.details;
match error_detail.status.as_str() { match error_detail.status.as_str() {
"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()) {
let limit = quota_info.quota_limit.as_deref().unwrap_or("unknown");
return GeminiError::QuotaExceeded {
limit: limit.to_string()
}; };
} }
GeminiError::QuotaExceeded { if let Some(quota_info) = details.iter().find(|d| d.quota_limit.is_some()) {
limit: "usage limit".to_string() let limit = quota_info.quota_limit.as_deref().unwrap_or("unknown");
return GeminiError::QuotaExceeded {
limit: limit.to_string(),
};
}
GeminiError::QuotaExceeded {
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 } status,
} message: error_detail.message,
"INTERNAL" => { },
GeminiError::InternalError {
details: error_detail.message
}
}
_ => {
GeminiError::ApiError {
status,
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,8 +207,9 @@ 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()
} }

View File

@@ -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);
} }
@@ -119,62 +121,77 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
fn handle_gemini_error(error: GeminiError) { fn handle_gemini_error(error: GeminiError) {
use colored::*; use colored::*;
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);
} }
} }
println!("\n{} Check the following:", "HINT:".yellow()); println!("\n{} Check the following:", "HINT:".yellow());
println!(" • Your GEMINI_API_KEY is correctly set"); println!(" • Your GEMINI_API_KEY is correctly set");
println!(" • Your internet connection is working"); println!(" • Your internet connection is working");

View File

@@ -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,9 +123,10 @@ 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(); {
return path.replacen("~", &home.to_string_lossy(), 1); let home = base_dirs.home_dir();
return path.replacen("~", &home.to_string_lossy(), 1);
} }
path.to_string() path.to_string()
} }