edgefirst_client/
storage.rs1use directories::ProjectDirs;
40use log::debug;
41use std::{path::PathBuf, sync::RwLock};
42
43#[derive(Debug)]
45pub enum StorageError {
46 NotAvailable(String),
48 ReadError(String),
50 WriteError(String),
52 ClearError(String),
54}
55
56impl std::fmt::Display for StorageError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 StorageError::NotAvailable(msg) => write!(f, "Token storage not available: {}", msg),
60 StorageError::ReadError(msg) => write!(f, "Failed to read token: {}", msg),
61 StorageError::WriteError(msg) => write!(f, "Failed to write token: {}", msg),
62 StorageError::ClearError(msg) => write!(f, "Failed to clear token: {}", msg),
63 }
64 }
65}
66
67impl std::error::Error for StorageError {}
68
69pub trait TokenStorage: Send + Sync {
109 fn store(&self, token: &str) -> Result<(), StorageError>;
111
112 fn load(&self) -> Result<Option<String>, StorageError>;
116
117 fn clear(&self) -> Result<(), StorageError>;
119}
120
121#[derive(Debug, Clone)]
140pub struct FileTokenStorage {
141 path: PathBuf,
142}
143
144impl FileTokenStorage {
145 pub fn new() -> Result<Self, StorageError> {
159 let path = ProjectDirs::from("ai", "EdgeFirst", "EdgeFirst Studio")
160 .ok_or_else(|| {
161 StorageError::NotAvailable("Could not determine user config directory".to_string())
162 })?
163 .config_dir()
164 .join("token");
165
166 debug!("FileTokenStorage using default path: {:?}", path);
167 Ok(Self { path })
168 }
169
170 pub fn with_path(path: PathBuf) -> Self {
172 debug!("FileTokenStorage using custom path: {:?}", path);
173 Self { path }
174 }
175
176 pub fn path(&self) -> &PathBuf {
178 &self.path
179 }
180}
181
182impl TokenStorage for FileTokenStorage {
183 fn store(&self, token: &str) -> Result<(), StorageError> {
184 if let Some(parent) = self.path.parent() {
186 std::fs::create_dir_all(parent).map_err(|e| {
187 StorageError::WriteError(format!("Failed to create directory {:?}: {}", parent, e))
188 })?;
189 }
190
191 std::fs::write(&self.path, token).map_err(|e| {
192 StorageError::WriteError(format!("Failed to write token to {:?}: {}", self.path, e))
193 })?;
194
195 debug!("Token stored to {:?}", self.path);
196 Ok(())
197 }
198
199 fn load(&self) -> Result<Option<String>, StorageError> {
200 if !self.path.exists() {
201 debug!("No token file found at {:?}", self.path);
202 return Ok(None);
203 }
204
205 let token = std::fs::read_to_string(&self.path).map_err(|e| {
206 StorageError::ReadError(format!("Failed to read token from {:?}: {}", self.path, e))
207 })?;
208
209 if token.is_empty() {
210 debug!("Token file at {:?} is empty", self.path);
211 return Ok(None);
212 }
213
214 debug!("Token loaded from {:?}", self.path);
215 Ok(Some(token))
216 }
217
218 fn clear(&self) -> Result<(), StorageError> {
219 if self.path.exists() {
220 std::fs::remove_file(&self.path).map_err(|e| {
221 StorageError::ClearError(format!(
222 "Failed to remove token file {:?}: {}",
223 self.path, e
224 ))
225 })?;
226 debug!("Token file removed from {:?}", self.path);
227 }
228 Ok(())
229 }
230}
231
232#[derive(Debug, Default)]
253pub struct MemoryTokenStorage {
254 token: RwLock<Option<String>>,
255}
256
257impl MemoryTokenStorage {
258 pub fn new() -> Self {
260 Self::default()
261 }
262}
263
264impl TokenStorage for MemoryTokenStorage {
265 fn store(&self, token: &str) -> Result<(), StorageError> {
266 let mut guard = self.token.write().map_err(|e| {
267 StorageError::WriteError(format!("Failed to acquire write lock: {}", e))
268 })?;
269 *guard = Some(token.to_string());
270 Ok(())
271 }
272
273 fn load(&self) -> Result<Option<String>, StorageError> {
274 let guard = self
275 .token
276 .read()
277 .map_err(|e| StorageError::ReadError(format!("Failed to acquire read lock: {}", e)))?;
278 Ok(guard.clone())
279 }
280
281 fn clear(&self) -> Result<(), StorageError> {
282 let mut guard = self.token.write().map_err(|e| {
283 StorageError::ClearError(format!("Failed to acquire write lock: {}", e))
284 })?;
285 *guard = None;
286 Ok(())
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use std::sync::Arc;
294 use tempfile::TempDir;
295
296 #[test]
297 fn test_memory_storage_store_load_clear() {
298 let storage = MemoryTokenStorage::new();
299
300 assert_eq!(storage.load().unwrap(), None);
302
303 storage.store("test-token").unwrap();
305 assert_eq!(storage.load().unwrap(), Some("test-token".to_string()));
306
307 storage.clear().unwrap();
309 assert_eq!(storage.load().unwrap(), None);
310 }
311
312 #[test]
313 fn test_memory_storage_overwrite() {
314 let storage = MemoryTokenStorage::new();
315
316 storage.store("token-1").unwrap();
317 assert_eq!(storage.load().unwrap(), Some("token-1".to_string()));
318
319 storage.store("token-2").unwrap();
320 assert_eq!(storage.load().unwrap(), Some("token-2".to_string()));
321 }
322
323 #[test]
324 fn test_memory_storage_thread_safety() {
325 let storage = Arc::new(MemoryTokenStorage::new());
326 let storage_clone = Arc::clone(&storage);
327
328 let handle = std::thread::spawn(move || {
329 storage_clone.store("thread-token").unwrap();
330 });
331
332 handle.join().unwrap();
333 assert_eq!(storage.load().unwrap(), Some("thread-token".to_string()));
334 }
335
336 #[test]
337 fn test_file_storage_store_load_clear() {
338 let temp_dir = TempDir::new().unwrap();
339 let token_path = temp_dir.path().join("token");
340 let storage = FileTokenStorage::with_path(token_path.clone());
341
342 assert_eq!(storage.load().unwrap(), None);
344
345 storage.store("file-test-token").unwrap();
347 assert!(token_path.exists());
348 assert_eq!(storage.load().unwrap(), Some("file-test-token".to_string()));
349
350 storage.clear().unwrap();
352 assert!(!token_path.exists());
353 assert_eq!(storage.load().unwrap(), None);
354 }
355
356 #[test]
357 fn test_file_storage_creates_parent_dirs() {
358 let temp_dir = TempDir::new().unwrap();
359 let token_path = temp_dir.path().join("nested").join("dirs").join("token");
360 let storage = FileTokenStorage::with_path(token_path.clone());
361
362 storage.store("nested-token").unwrap();
363 assert!(token_path.exists());
364 assert_eq!(storage.load().unwrap(), Some("nested-token".to_string()));
365 }
366
367 #[test]
368 fn test_file_storage_overwrite() {
369 let temp_dir = TempDir::new().unwrap();
370 let token_path = temp_dir.path().join("token");
371 let storage = FileTokenStorage::with_path(token_path);
372
373 storage.store("token-1").unwrap();
374 assert_eq!(storage.load().unwrap(), Some("token-1".to_string()));
375
376 storage.store("token-2").unwrap();
377 assert_eq!(storage.load().unwrap(), Some("token-2".to_string()));
378 }
379
380 #[test]
381 fn test_file_storage_clear_nonexistent() {
382 let temp_dir = TempDir::new().unwrap();
383 let token_path = temp_dir.path().join("nonexistent_token");
384 let storage = FileTokenStorage::with_path(token_path);
385
386 assert!(storage.clear().is_ok());
388 }
389
390 #[test]
391 fn test_file_storage_path() {
392 let path = PathBuf::from("/custom/path/token");
393 let storage = FileTokenStorage::with_path(path.clone());
394 assert_eq!(storage.path(), &path);
395 }
396
397 #[test]
398 fn test_storage_error_display() {
399 let err = StorageError::NotAvailable("test".to_string());
400 assert!(err.to_string().contains("test"));
401 assert!(err.to_string().contains("not available"));
402
403 let err = StorageError::ReadError("read failed".to_string());
404 assert!(err.to_string().contains("read failed"));
405
406 let err = StorageError::WriteError("write failed".to_string());
407 assert!(err.to_string().contains("write failed"));
408
409 let err = StorageError::ClearError("clear failed".to_string());
410 assert!(err.to_string().contains("clear failed"));
411 }
412}