From 3a4044d2ab7352b8fdc140ac2ca048a0d88d9676 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 10 May 2026 10:46:51 -0400 Subject: [PATCH] implementation --- .../core/providers/provider_credential_key.rs | 2 +- .../provider_credential_loading_cache.rs | 154 ++++++++++++++++-- 2 files changed, 142 insertions(+), 14 deletions(-) diff --git a/crates/desktop/artcraft/src/core/providers/provider_credential_key.rs b/crates/desktop/artcraft/src/core/providers/provider_credential_key.rs index 39e6d1cefa..aae70387ff 100644 --- a/crates/desktop/artcraft/src/core/providers/provider_credential_key.rs +++ b/crates/desktop/artcraft/src/core/providers/provider_credential_key.rs @@ -1,7 +1,7 @@ use serde_derive::{Deserialize, Serialize}; use crate::core::providers::provider_credential_type::ProviderCredentialType; -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ProviderCredentialKey { diff --git a/crates/desktop/artcraft/src/core/providers/provider_credential_loading_cache.rs b/crates/desktop/artcraft/src/core/providers/provider_credential_loading_cache.rs index 431cca8fe5..a9c5d498dc 100644 --- a/crates/desktop/artcraft/src/core/providers/provider_credential_loading_cache.rs +++ b/crates/desktop/artcraft/src/core/providers/provider_credential_loading_cache.rs @@ -1,46 +1,174 @@ +use std::collections::HashMap; use std::error::Error; use std::fmt::Display; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, Instant}; + +use log::{info, warn}; + +use crate::core::providers::payload::api_key::{ApiKeyData, ApiKeyDataError}; use crate::core::providers::payload::provider_credential_payload::ProviderCredentialPayload; +use crate::core::providers::payload::web_login::{WebLoginData, WebLoginDataError}; use crate::core::providers::provider_credential_key::ProviderCredentialKey; +use crate::core::providers::provider_credential_type::ProviderCredentialType; use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; + +struct CacheEntry { + payload: ProviderCredentialPayload, + loaded_at: Instant, +} #[derive(Clone)] pub struct ProviderCredentialLoadingCache { - // TODO: Some kind of Arc>> + inner: Arc>>, app_data_root: AppDataRoot, + ttl: Duration, } impl ProviderCredentialLoadingCache { - pub fn new(app_data_root: AppDataRoot) -> Self { Self { + inner: Arc::new(RwLock::new(HashMap::new())), app_data_root, - } + ttl: Duration::from_secs(300), // 5 minutes + } } - - pub fn get_credentials(&self, key: ProviderCredentialKey) -> Result, ProviderCredentialLoadingCacheError> { - unimplemented!() + + pub fn new_with_ttl(app_data_root: AppDataRoot, ttl: Duration) -> Self { + Self { + inner: Arc::new(RwLock::new(HashMap::new())), + app_data_root, + ttl, + } } - - pub fn save_credentials(&self, key: ProviderCredentialKey, payload: ProviderCredentialPayload) -> Result<(), ProviderCredentialLoadingCacheError> { - unimplemented!() + + pub fn get_credentials( + &self, + key: ProviderCredentialKey, + ) -> Result, ProviderCredentialLoadingCacheError> { + // Check cache first. + { + let cache = self.inner.read() + .map_err(|_| ProviderCredentialLoadingCacheError::LockPoisoned)?; + + if let Some(entry) = cache.get(&key) { + if entry.loaded_at.elapsed() < self.ttl { + return Ok(Some(entry.payload.clone())); + } + } + } + + // Cache miss or expired — load from disk. + let file_path = self.app_data_root.credentials_dir().path().join(key.get_filename()); + + if !file_path.exists() { + return Ok(None); + } + + info!("Loading credential from disk: {:?}", file_path); + + let payload = match key.get_type() { + ProviderCredentialType::ApiKey => { + let data = ApiKeyData::load_from_file(&file_path) + .map_err(ProviderCredentialLoadingCacheError::ApiKeyError)?; + ProviderCredentialPayload::ApiKey(data) + } + ProviderCredentialType::WebLogin => { + let data = WebLoginData::load_from_file(&file_path) + .map_err(ProviderCredentialLoadingCacheError::WebLoginError)?; + ProviderCredentialPayload::WebLogin(data) + } + }; + + // Store in cache. + { + let mut cache = self.inner.write() + .map_err(|_| ProviderCredentialLoadingCacheError::LockPoisoned)?; + + cache.insert(key, CacheEntry { + payload: payload.clone(), + loaded_at: Instant::now(), + }); + } + + Ok(Some(payload)) + } + + pub fn save_credentials( + &self, + key: ProviderCredentialKey, + payload: ProviderCredentialPayload, + ) -> Result<(), ProviderCredentialLoadingCacheError> { + let file_path = self.app_data_root.credentials_dir().path().join(key.get_filename()); + + info!("Saving credential to disk: {:?}", file_path); + + match (&payload, key.get_type()) { + (ProviderCredentialPayload::ApiKey(data), ProviderCredentialType::ApiKey) => { + data.save_to_file(&file_path) + .map_err(ProviderCredentialLoadingCacheError::ApiKeyError)?; + } + (ProviderCredentialPayload::WebLogin(data), ProviderCredentialType::WebLogin) => { + data.save_to_file(&file_path) + .map_err(ProviderCredentialLoadingCacheError::WebLoginError)?; + } + _ => { + warn!("Credential type mismatch for key {:?}", key); + return Err(ProviderCredentialLoadingCacheError::TypeMismatch); + } + } + + // Update cache. + { + let mut cache = self.inner.write() + .map_err(|_| ProviderCredentialLoadingCacheError::LockPoisoned)?; + + cache.insert(key, CacheEntry { + payload, + loaded_at: Instant::now(), + }); + } + + Ok(()) + } + + /// Invalidate a cached entry, forcing a re-read from disk on next access. + pub fn invalidate(&self, key: ProviderCredentialKey) { + if let Ok(mut cache) = self.inner.write() { + cache.remove(&key); + } } } #[derive(Debug)] pub enum ProviderCredentialLoadingCacheError { IoError(std::io::Error), + ApiKeyError(ApiKeyDataError), + WebLoginError(WebLoginDataError), + TypeMismatch, + LockPoisoned, } impl Error for ProviderCredentialLoadingCacheError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { - ProviderCredentialLoadingCacheError::IoError(err) => Some(err), + Self::IoError(err) => Some(err), + Self::ApiKeyError(err) => Some(err), + Self::WebLoginError(err) => Some(err), + _ => None, } - } + } } impl Display for ProviderCredentialLoadingCacheError { - + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(e) => write!(f, "IO error: {}", e), + Self::ApiKeyError(e) => write!(f, "API key error: {}", e), + Self::WebLoginError(e) => write!(f, "Web login error: {}", e), + Self::TypeMismatch => write!(f, "Credential type does not match key type"), + Self::LockPoisoned => write!(f, "Internal lock poisoned"), + } + } } -