diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2b3ce85..beeae08 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use tauri::{AppHandle, State}; use tauri_plugin_http::reqwest; use tauri_plugin_store::StoreExt; @@ -6,6 +7,7 @@ use crate::achievements::{get_achievement_info, load_achievements, AchievementUn use crate::bridge_manager::SharedBridgeManager; use crate::config::{ClaudeStartOptions, HikariConfig}; use crate::stats::UsageStats; +use crate::temp_manager::SharedTempFileManager; const CONFIG_STORE_KEY: &str = "config"; @@ -298,3 +300,76 @@ pub async fn check_for_updates() -> Result { release_notes: latest.body.clone(), }) } + +#[derive(Debug, Clone, serde::Serialize)] +pub struct SavedFileInfo { + pub path: String, + pub filename: String, +} + +#[tauri::command] +pub async fn save_temp_file( + temp_manager: State<'_, SharedTempFileManager>, + conversation_id: String, + data: Vec, + filename: Option, +) -> Result { + let mut manager = temp_manager.lock(); + let path = manager.save_file(&conversation_id, &data, filename.as_deref())?; + + let filename = path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + Ok(SavedFileInfo { + path: path.to_string_lossy().to_string(), + filename, + }) +} + +#[tauri::command] +pub async fn register_temp_file( + temp_manager: State<'_, SharedTempFileManager>, + conversation_id: String, + file_path: String, +) -> Result<(), String> { + let mut manager = temp_manager.lock(); + manager.register_file(&conversation_id, PathBuf::from(file_path)); + Ok(()) +} + +#[tauri::command] +pub async fn get_temp_files( + temp_manager: State<'_, SharedTempFileManager>, + conversation_id: String, +) -> Result, String> { + let manager = temp_manager.lock(); + let files = manager.get_files_for_conversation(&conversation_id); + Ok(files.iter().map(|p| p.to_string_lossy().to_string()).collect()) +} + +#[tauri::command] +pub async fn cleanup_temp_files( + temp_manager: State<'_, SharedTempFileManager>, + conversation_id: String, +) -> Result<(), String> { + let mut manager = temp_manager.lock(); + manager.cleanup_conversation(&conversation_id) +} + +#[tauri::command] +pub async fn cleanup_all_temp_files( + temp_manager: State<'_, SharedTempFileManager>, +) -> Result<(), String> { + let mut manager = temp_manager.lock(); + manager.cleanup_all() +} + +#[tauri::command] +pub async fn cleanup_orphaned_temp_files( + temp_manager: State<'_, SharedTempFileManager>, +) -> Result { + let mut manager = temp_manager.lock(); + manager.cleanup_orphaned_files() +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 685d3da..c402e7f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ mod commands; mod config; mod notifications; mod stats; +mod temp_manager; mod types; mod vbs_notification; mod windows_toast; @@ -14,6 +15,7 @@ use bridge_manager::create_shared_bridge_manager; use commands::load_saved_achievements; use commands::*; use notifications::*; +use temp_manager::create_shared_temp_manager; use vbs_notification::*; use windows_toast::*; use wsl_notifications::*; @@ -21,6 +23,7 @@ use wsl_notifications::*; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let bridge_manager = create_shared_bridge_manager(); + let temp_manager = create_shared_temp_manager().expect("Failed to create temp file manager"); tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) @@ -31,9 +34,18 @@ pub fn run() { .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_http::init()) .manage(bridge_manager.clone()) + .manage(temp_manager.clone()) .setup(move |app| { // Initialize the app handle in the bridge manager bridge_manager.lock().set_app_handle(app.handle().clone()); + + // Clean up any orphaned temp files from previous sessions + if let Ok(count) = temp_manager.lock().cleanup_orphaned_files() { + if count > 0 { + println!("Cleaned up {} orphaned temp files", count); + } + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -58,6 +70,12 @@ pub fn run() { validate_directory, list_skills, check_for_updates, + save_temp_file, + register_temp_file, + get_temp_files, + cleanup_temp_files, + cleanup_all_temp_files, + cleanup_orphaned_temp_files, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/temp_manager.rs b/src-tauri/src/temp_manager.rs new file mode 100644 index 0000000..cfc8cea --- /dev/null +++ b/src-tauri/src/temp_manager.rs @@ -0,0 +1,139 @@ +use parking_lot::Mutex; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use uuid::Uuid; + +const TEMP_DIR_NAME: &str = "hikari-uploads"; + +pub struct TempFileManager { + base_dir: PathBuf, + files: HashMap>, +} + +impl TempFileManager { + pub fn new() -> Result { + let base_dir = std::env::temp_dir().join(TEMP_DIR_NAME); + + if !base_dir.exists() { + fs::create_dir_all(&base_dir) + .map_err(|e| format!("Failed to create temp directory: {}", e))?; + } + + Ok(TempFileManager { + base_dir, + files: HashMap::new(), + }) + } + + #[allow(dead_code)] + pub fn get_base_dir(&self) -> &Path { + &self.base_dir + } + + pub fn save_file( + &mut self, + conversation_id: &str, + data: &[u8], + original_filename: Option<&str>, + ) -> Result { + let unique_id = Uuid::new_v4(); + let extension = original_filename + .and_then(|name| Path::new(name).extension()) + .and_then(|ext| ext.to_str()) + .unwrap_or("bin"); + + let filename = format!("{}_{}.{}", conversation_id, unique_id, extension); + let file_path = self.base_dir.join(&filename); + + fs::write(&file_path, data) + .map_err(|e| format!("Failed to write temp file: {}", e))?; + + self.files + .entry(conversation_id.to_string()) + .or_default() + .push(file_path.clone()); + + Ok(file_path) + } + + pub fn register_file(&mut self, conversation_id: &str, file_path: PathBuf) { + self.files + .entry(conversation_id.to_string()) + .or_default() + .push(file_path); + } + + pub fn get_files_for_conversation(&self, conversation_id: &str) -> Vec { + self.files + .get(conversation_id) + .cloned() + .unwrap_or_default() + } + + pub fn cleanup_conversation(&mut self, conversation_id: &str) -> Result<(), String> { + if let Some(files) = self.files.remove(conversation_id) { + for file_path in files { + if file_path.exists() { + if let Err(e) = fs::remove_file(&file_path) { + eprintln!( + "Warning: Failed to remove temp file {:?}: {}", + file_path, e + ); + } + } + } + } + Ok(()) + } + + pub fn cleanup_all(&mut self) -> Result<(), String> { + let conversation_ids: Vec = self.files.keys().cloned().collect(); + + for conversation_id in conversation_ids { + self.cleanup_conversation(&conversation_id)?; + } + + Ok(()) + } + + pub fn cleanup_orphaned_files(&mut self) -> Result { + let mut cleaned_count = 0; + + if !self.base_dir.exists() { + return Ok(0); + } + + let tracked_files: std::collections::HashSet = + self.files.values().flatten().cloned().collect(); + + let entries = fs::read_dir(&self.base_dir) + .map_err(|e| format!("Failed to read temp directory: {}", e))?; + + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() && !tracked_files.contains(&path) { + if let Err(e) = fs::remove_file(&path) { + eprintln!("Warning: Failed to remove orphaned file {:?}: {}", path, e); + } else { + cleaned_count += 1; + } + } + } + + Ok(cleaned_count) + } +} + +impl Default for TempFileManager { + fn default() -> Self { + Self::new().expect("Failed to create TempFileManager") + } +} + +pub type SharedTempFileManager = Arc>; + +pub fn create_shared_temp_manager() -> Result { + Ok(Arc::new(Mutex::new(TempFileManager::new()?))) +} diff --git a/src/lib/stores/conversations.ts b/src/lib/stores/conversations.ts index 6948aaf..243d22a 100644 --- a/src/lib/stores/conversations.ts +++ b/src/lib/stores/conversations.ts @@ -272,7 +272,7 @@ function createConversationsStore() { return newConv.id; }, - deleteConversation: (id: string) => { + deleteConversation: async (id: string) => { ensureInitialized(); const convs = get(conversations); const activeId = get(activeConversationId); @@ -282,8 +282,8 @@ function createConversationsStore() { return false; } - // Clean up tracking for this conversation - cleanupConversationTracking(id); + // Clean up tracking for this conversation (including temp files) + await cleanupConversationTracking(id); conversations.update((c) => { c.delete(id); diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index a1f7ec5..d14ded8 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -107,8 +107,15 @@ interface WorkingDirectoryPayload { conversation_id?: string; } -export function cleanupConversationTracking(conversationId: string) { +export async function cleanupConversationTracking(conversationId: string) { connectedConversations.delete(conversationId); + + // Clean up any temp files associated with this conversation + try { + await invoke("cleanup_temp_files", { conversationId }); + } catch (error) { + console.error("Failed to cleanup temp files for conversation:", error); + } } export async function initializeTauriListeners() {