| use std::fs; |
| use std::path::PathBuf; |
| use uuid::Uuid; |
|
|
| use crate::models::{Account, AccountIndex, AccountSummary, TokenData, QuotaData}; |
| use crate::modules; |
| use once_cell::sync::Lazy; |
| use std::sync::Mutex; |
|
|
| |
| static ACCOUNT_INDEX_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(())); |
|
|
| const DATA_DIR_LOCAL: &str = ".antigravity_tools"; |
| const DATA_DIR_CLOUD: &str = "/data"; |
| const ACCOUNTS_INDEX: &str = "accounts.json"; |
| const ACCOUNTS_DIR: &str = "accounts"; |
|
|
| |
| |
| pub fn get_data_dir() -> Result<PathBuf, String> { |
| |
| let cloud_path = PathBuf::from(DATA_DIR_CLOUD); |
| if cloud_path.exists() { |
| modules::logger::log_info(&format!("[get_data_dir] Using cloud path: {:?}", cloud_path)); |
| return Ok(cloud_path); |
| } |
|
|
| |
| let cwd_data = PathBuf::from("./data"); |
| if cwd_data.exists() { |
| let canonical = cwd_data.canonicalize() |
| .map_err(|e| format!("Failed to canonicalize ./data: {}", e))?; |
| modules::logger::log_info(&format!("[get_data_dir] Using cwd data path: {:?}", canonical)); |
| return Ok(canonical); |
| } |
|
|
| |
| let home = dirs::home_dir().ok_or("Cannot get user home directory")?; |
| let data_dir = home.join(DATA_DIR_LOCAL); |
|
|
| |
| if !data_dir.exists() { |
| fs::create_dir_all(&data_dir) |
| .map_err(|e| format!("Failed to create data directory: {}", e))?; |
| } |
|
|
| modules::logger::log_info(&format!("[get_data_dir] Using local path: {:?}", data_dir)); |
| Ok(data_dir) |
| } |
|
|
| |
| pub fn get_accounts_dir() -> Result<PathBuf, String> { |
| let data_dir = get_data_dir()?; |
| let accounts_dir = data_dir.join(ACCOUNTS_DIR); |
|
|
| if !accounts_dir.exists() { |
| fs::create_dir_all(&accounts_dir) |
| .map_err(|e| format!("Failed to create accounts directory: {}", e))?; |
| } |
|
|
| Ok(accounts_dir) |
| } |
|
|
| |
| pub fn load_account_index() -> Result<AccountIndex, String> { |
| let data_dir = get_data_dir()?; |
| let index_path = data_dir.join(ACCOUNTS_INDEX); |
|
|
| if !index_path.exists() { |
| modules::logger::log_warn("Account index file does not exist"); |
| return Ok(AccountIndex::new()); |
| } |
|
|
| let content = fs::read_to_string(&index_path) |
| .map_err(|e| format!("Failed to read account index: {}", e))?; |
|
|
| let index: AccountIndex = serde_json::from_str(&content) |
| .map_err(|e| format!("Failed to parse account index: {}", e))?; |
|
|
| modules::logger::log_info(&format!("Loaded index with {} accounts", index.accounts.len())); |
| Ok(index) |
| } |
|
|
| |
| pub fn save_account_index(index: &AccountIndex) -> Result<(), String> { |
| let data_dir = get_data_dir()?; |
| let index_path = data_dir.join(ACCOUNTS_INDEX); |
| let temp_path = data_dir.join(format!("{}.tmp", ACCOUNTS_INDEX)); |
|
|
| modules::logger::log_info(&format!("[save_account_index] Saving to: {:?}", index_path)); |
|
|
| let content = serde_json::to_string_pretty(index) |
| .map_err(|e| format!("Failed to serialize account index: {}", e))?; |
|
|
| |
| fs::write(&temp_path, &content) |
| .map_err(|e| format!("Failed to write temp index file: {}", e))?; |
|
|
| |
| fs::rename(&temp_path, &index_path) |
| .map_err(|e| format!("Failed to replace index file: {}", e))?; |
|
|
| modules::logger::log_info(&format!("[save_account_index] Saved {} accounts successfully", index.accounts.len())); |
| Ok(()) |
| } |
|
|
| |
| pub fn load_account(account_id: &str) -> Result<Account, String> { |
| let accounts_dir = get_accounts_dir()?; |
| let account_path = accounts_dir.join(format!("{}.json", account_id)); |
|
|
| if !account_path.exists() { |
| return Err(format!("Account not found: {}", account_id)); |
| } |
|
|
| let content = fs::read_to_string(&account_path) |
| .map_err(|e| format!("Failed to read account data: {}", e))?; |
|
|
| serde_json::from_str(&content) |
| .map_err(|e| format!("Failed to parse account data: {}", e)) |
| } |
|
|
| |
| pub fn save_account(account: &Account) -> Result<(), String> { |
| let accounts_dir = get_accounts_dir()?; |
| let account_path = accounts_dir.join(format!("{}.json", account.id)); |
|
|
| let content = serde_json::to_string_pretty(account) |
| .map_err(|e| format!("Failed to serialize account data: {}", e))?; |
|
|
| fs::write(&account_path, content) |
| .map_err(|e| format!("Failed to save account data: {}", e)) |
| } |
|
|
| |
| pub fn list_accounts() -> Result<Vec<Account>, String> { |
| modules::logger::log_info("Listing accounts..."); |
| let mut index = load_account_index()?; |
| let mut accounts = Vec::new(); |
| let mut invalid_ids = Vec::new(); |
|
|
| for summary in &index.accounts { |
| match load_account(&summary.id) { |
| Ok(account) => accounts.push(account), |
| Err(e) => { |
| modules::logger::log_error(&format!("Failed to load account {}: {}", summary.id, e)); |
| if e.contains("Account not found") || e.contains("Os { code: 2,") || e.contains("No such file") { |
| invalid_ids.push(summary.id.clone()); |
| } |
| }, |
| } |
| } |
|
|
| |
| if !invalid_ids.is_empty() { |
| modules::logger::log_warn(&format!("Found {} invalid account indexes, cleaning up...", invalid_ids.len())); |
|
|
| index.accounts.retain(|s| !invalid_ids.contains(&s.id)); |
|
|
| if let Some(current_id) = &index.current_account_id { |
| if invalid_ids.contains(current_id) { |
| index.current_account_id = index.accounts.first().map(|s| s.id.clone()); |
| } |
| } |
|
|
| if let Err(e) = save_account_index(&index) { |
| modules::logger::log_error(&format!("Failed to clean up index: {}", e)); |
| } else { |
| modules::logger::log_info("Index cleanup completed"); |
| } |
| } |
|
|
| Ok(accounts) |
| } |
|
|
| |
| pub fn add_account(email: String, name: Option<String>, token: TokenData) -> Result<Account, String> { |
| let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?; |
| let mut index = load_account_index()?; |
|
|
| |
| if index.accounts.iter().any(|s| s.email == email) { |
| return Err(format!("Account already exists: {}", email)); |
| } |
|
|
| |
| let account_id = Uuid::new_v4().to_string(); |
| let mut account = Account::new(account_id.clone(), email.clone(), token); |
| account.name = name.clone(); |
|
|
| |
| save_account(&account)?; |
|
|
| |
| index.accounts.push(AccountSummary { |
| id: account_id.clone(), |
| email: email.clone(), |
| name: name.clone(), |
| created_at: account.created_at, |
| last_used: account.last_used, |
| }); |
|
|
| |
| if index.current_account_id.is_none() { |
| index.current_account_id = Some(account_id); |
| } |
|
|
| save_account_index(&index)?; |
|
|
| Ok(account) |
| } |
|
|
| |
| pub fn upsert_account(email: String, name: Option<String>, token: TokenData) -> Result<Account, String> { |
| let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?; |
| let mut index = load_account_index()?; |
|
|
| |
| let existing_account_id = index.accounts.iter() |
| .find(|s| s.email == email) |
| .map(|s| s.id.clone()); |
|
|
| if let Some(account_id) = existing_account_id { |
| |
| match load_account(&account_id) { |
| Ok(mut account) => { |
| account.token = token; |
| account.name = name.clone(); |
| account.update_last_used(); |
| save_account(&account)?; |
|
|
| |
| if let Some(idx_summary) = index.accounts.iter_mut().find(|s| s.id == account_id) { |
| idx_summary.name = name; |
| save_account_index(&index)?; |
| } |
|
|
| return Ok(account); |
| }, |
| Err(e) => { |
| modules::logger::log_warn(&format!("Account {} file missing ({}), recreating...", account_id, e)); |
| |
| let mut account = Account::new(account_id.clone(), email.clone(), token); |
| account.name = name.clone(); |
| save_account(&account)?; |
|
|
| if let Some(idx_summary) = index.accounts.iter_mut().find(|s| s.id == account_id) { |
| idx_summary.name = name; |
| save_account_index(&index)?; |
| } |
|
|
| return Ok(account); |
| } |
| } |
| } |
|
|
| |
| drop(_lock); |
| add_account(email, name, token) |
| } |
|
|
| |
| pub fn delete_account(account_id: &str) -> Result<(), String> { |
| let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?; |
| let mut index = load_account_index()?; |
|
|
| |
| let original_len = index.accounts.len(); |
| index.accounts.retain(|s| s.id != account_id); |
|
|
| if index.accounts.len() == original_len { |
| return Err(format!("Account ID not found: {}", account_id)); |
| } |
|
|
| |
| if index.current_account_id.as_deref() == Some(account_id) { |
| index.current_account_id = index.accounts.first().map(|s| s.id.clone()); |
| } |
|
|
| save_account_index(&index)?; |
|
|
| |
| let accounts_dir = get_accounts_dir()?; |
| let account_path = accounts_dir.join(format!("{}.json", account_id)); |
|
|
| if account_path.exists() { |
| fs::remove_file(&account_path) |
| .map_err(|e| format!("Failed to delete account file: {}", e))?; |
| } |
|
|
| Ok(()) |
| } |
|
|
| |
| pub fn delete_accounts(account_ids: &[String]) -> Result<(), String> { |
| let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?; |
| let mut index = load_account_index()?; |
|
|
| let accounts_dir = get_accounts_dir()?; |
|
|
| for account_id in account_ids { |
| |
| index.accounts.retain(|s| &s.id != account_id); |
|
|
| |
| if index.current_account_id.as_deref() == Some(account_id) { |
| index.current_account_id = None; |
| } |
|
|
| |
| let account_path = accounts_dir.join(format!("{}.json", account_id)); |
| if account_path.exists() { |
| let _ = fs::remove_file(&account_path); |
| } |
| } |
|
|
| |
| if index.current_account_id.is_none() { |
| index.current_account_id = index.accounts.first().map(|s| s.id.clone()); |
| } |
|
|
| save_account_index(&index) |
| } |
|
|
| |
| pub fn get_current_account_id() -> Result<Option<String>, String> { |
| let index = load_account_index()?; |
| Ok(index.current_account_id) |
| } |
|
|
| |
| pub fn get_current_account() -> Result<Option<Account>, String> { |
| if let Some(id) = get_current_account_id()? { |
| Ok(Some(load_account(&id)?)) |
| } else { |
| Ok(None) |
| } |
| } |
|
|
| |
| pub fn set_current_account_id(account_id: &str) -> Result<(), String> { |
| let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?; |
| let mut index = load_account_index()?; |
| index.current_account_id = Some(account_id.to_string()); |
| save_account_index(&index) |
| } |
|
|
| |
| pub fn update_account_quota(account_id: &str, quota: QuotaData) -> Result<(), String> { |
| let mut account = load_account(account_id)?; |
| account.update_quota(quota); |
| save_account(&account) |
| } |
|
|
| |
| #[allow(dead_code)] |
| pub fn export_accounts() -> Result<Vec<(String, String)>, String> { |
| let accounts = list_accounts()?; |
| let mut exports = Vec::new(); |
|
|
| for account in accounts { |
| exports.push((account.email, account.token.refresh_token)); |
| } |
|
|
| Ok(exports) |
| } |
|
|
| |
| pub async fn fetch_quota_with_retry(account: &mut Account) -> crate::error::AppResult<QuotaData> { |
| use crate::modules::oauth; |
| use crate::error::AppError; |
| use reqwest::StatusCode; |
|
|
| |
| let token = oauth::ensure_fresh_token(&account.token).await.map_err(AppError::OAuth)?; |
|
|
| if token.access_token != account.token.access_token { |
| modules::logger::log_info(&format!("Token refreshed for: {}", account.email)); |
| account.token = token.clone(); |
|
|
| |
| let name = if account.name.is_none() || account.name.as_ref().map_or(false, |n| n.trim().is_empty()) { |
| match oauth::get_user_info(&token.access_token).await { |
| Ok(user_info) => user_info.get_display_name(), |
| Err(_) => None |
| } |
| } else { |
| account.name.clone() |
| }; |
|
|
| account.name = name.clone(); |
| upsert_account(account.email.clone(), name, token.clone()).map_err(AppError::Account)?; |
| } |
|
|
| |
| if account.name.is_none() || account.name.as_ref().map_or(false, |n| n.trim().is_empty()) { |
| modules::logger::log_info(&format!("Account {} missing name, fetching...", account.email)); |
| match oauth::get_user_info(&account.token.access_token).await { |
| Ok(user_info) => { |
| let display_name = user_info.get_display_name(); |
| modules::logger::log_info(&format!("Got user name: {:?}", display_name)); |
| account.name = display_name.clone(); |
| if let Err(e) = upsert_account(account.email.clone(), display_name, account.token.clone()) { |
| modules::logger::log_warn(&format!("Failed to save user name: {}", e)); |
| } |
| }, |
| Err(e) => { |
| modules::logger::log_warn(&format!("Failed to get user name: {}", e)); |
| } |
| } |
| } |
|
|
| |
| let result: crate::error::AppResult<(QuotaData, Option<String>)> = modules::fetch_quota(&account.token.access_token, &account.email).await; |
|
|
| |
| if let Ok((ref _q, ref project_id)) = result { |
| if project_id.is_some() && *project_id != account.token.project_id { |
| modules::logger::log_info(&format!("Detected project_id update ({}), saving...", account.email)); |
| account.token.project_id = project_id.clone(); |
| if let Err(e) = upsert_account(account.email.clone(), account.name.clone(), account.token.clone()) { |
| modules::logger::log_warn(&format!("Failed to save project_id: {}", e)); |
| } |
| } |
| } |
|
|
| |
| if let Err(AppError::Network(ref e)) = result { |
| if let Some(status) = e.status() { |
| if status == StatusCode::UNAUTHORIZED { |
| modules::logger::log_warn(&format!("401 Unauthorized for {}, forcing refresh...", account.email)); |
|
|
| |
| let token_res = oauth::refresh_access_token(&account.token.refresh_token) |
| .await |
| .map_err(AppError::OAuth)?; |
|
|
| let new_token = TokenData::new( |
| token_res.access_token.clone(), |
| account.token.refresh_token.clone(), |
| token_res.expires_in, |
| account.token.email.clone(), |
| account.token.project_id.clone(), |
| None, |
| ); |
|
|
| |
| let name = if account.name.is_none() || account.name.as_ref().map_or(false, |n| n.trim().is_empty()) { |
| match oauth::get_user_info(&token_res.access_token).await { |
| Ok(user_info) => user_info.get_display_name(), |
| Err(_) => None |
| } |
| } else { |
| account.name.clone() |
| }; |
|
|
| account.token = new_token.clone(); |
| account.name = name.clone(); |
| upsert_account(account.email.clone(), name, new_token.clone()).map_err(AppError::Account)?; |
|
|
| |
| let retry_result: crate::error::AppResult<(QuotaData, Option<String>)> = modules::fetch_quota(&new_token.access_token, &account.email).await; |
|
|
| |
| if let Ok((ref _q, ref project_id)) = retry_result { |
| if project_id.is_some() && *project_id != account.token.project_id { |
| modules::logger::log_info(&format!("Detected retry project_id update ({}), saving...", account.email)); |
| account.token.project_id = project_id.clone(); |
| let _ = upsert_account(account.email.clone(), account.name.clone(), account.token.clone()); |
| } |
| } |
|
|
| if let Err(AppError::Network(ref e)) = retry_result { |
| if let Some(s) = e.status() { |
| if s == StatusCode::FORBIDDEN { |
| let mut q = QuotaData::new(); |
| q.is_forbidden = true; |
| return Ok(q); |
| } |
| } |
| } |
| return retry_result.map(|(q, _)| q); |
| } |
| } |
| } |
|
|
| result.map(|(q, _)| q) |
| } |
|
|