From 4cc8de8a1abffb062ad571829df15656cf719cbc Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sun, 12 Apr 2026 01:27:03 +0200 Subject: [PATCH] fix: Settings persistent (kein Keyring) + Single-Instance Config-Persistenz: - Passwort wird base64-kodiert in config.json gespeichert (statt OS-Keyring der beim Cross-Compile nicht funktioniert) - Config-Pfad wird beim Laden/Speichern geloggt fuer Debugging - Keyring-Dependency entfernt, base64 hinzugefuegt Single-Instance: - Lock-File in Config-Dir verhindert doppelte Instanz - Wenn .cloud Datei doppelgeklickt wird und Client laeuft: Pfad wird in open_request.txt geschrieben und 2. Instanz beendet sich - Laufende Instanz pollt open_request.txt und oeffnet die Datei - Fenster wird automatisch in den Vordergrund geholt Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/desktop/src-tauri/Cargo.toml | 2 +- clients/desktop/src-tauri/src/lib.rs | 70 +++++++++++++++++--- clients/desktop/src-tauri/src/sync/config.rs | 61 ++++++++++------- 3 files changed, 100 insertions(+), 33 deletions(-) diff --git a/clients/desktop/src-tauri/Cargo.toml b/clients/desktop/src-tauri/Cargo.toml index 743caaa..311e4c5 100644 --- a/clients/desktop/src-tauri/Cargo.toml +++ b/clients/desktop/src-tauri/Cargo.toml @@ -26,5 +26,5 @@ sha2 = "0.10" dirs = "6" rusqlite = { version = "0.34", features = ["bundled"] } chrono = { version = "0.4", features = ["serde"] } -keyring = "3" +base64 = "0.22" open = "5" diff --git a/clients/desktop/src-tauri/src/lib.rs b/clients/desktop/src-tauri/src/lib.rs index f9c0834..6361dd9 100644 --- a/clients/desktop/src-tauri/src/lib.rs +++ b/clients/desktop/src-tauri/src/lib.rs @@ -39,11 +39,11 @@ async fn login( *state.api.lock().unwrap() = Some(api); *state.username.lock().unwrap() = Some(username.clone()); - // Save credentials - let _ = AppConfig::save_password(&server_url, &username, &password); + // Save credentials to config file let mut config = AppConfig::load(); config.server_url = server_url; config.username = username; + config.save_password(&password); let _ = config.save(); Ok(serde_json::json!({ @@ -57,11 +57,9 @@ async fn login( #[tauri::command] fn load_saved_config() -> Result { let config = AppConfig::load(); - let has_credentials = if !config.server_url.is_empty() && !config.username.is_empty() { - AppConfig::load_password(&config.server_url, &config.username).is_some() - } else { - false - }; + let has_credentials = !config.server_url.is_empty() + && !config.username.is_empty() + && config.get_password().is_some(); Ok(serde_json::json!({ "server_url": config.server_url, @@ -79,8 +77,8 @@ async fn auto_login(state: State<'_, AppState>) -> Result = std::env::args().collect(); + + // If we have a .cloud file argument and another instance is running + if args.len() > 1 && args[1].ends_with(".cloud") { + if lock_file.exists() { + // Write the .cloud path to a request file for the running instance + let request_file = config_dir.join("open_request.txt"); + std::fs::write(&request_file, &args[1]).ok(); + eprintln!("[SingleInstance] Passing {} to running instance", args[1]); + std::process::exit(0); + } + } + + // Write our lock file + std::fs::write(&lock_file, std::process::id().to_string()).ok(); + +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + handle_single_instance(); + tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_notification::init()) @@ -558,6 +584,32 @@ pub fn run() { // Use window icon for tray (set via tauri.conf.json) let icon = app.default_window_icon().cloned(); + // Watch for open requests from other instances + let app_req = app.handle().clone(); + std::thread::spawn(move || { + let config_dir = dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("MiniCloud Sync"); + let request_file = config_dir.join("open_request.txt"); + loop { + std::thread::sleep(Duration::from_secs(1)); + if request_file.exists() { + if let Ok(path) = std::fs::read_to_string(&request_file) { + let path = path.trim().to_string(); + if !path.is_empty() { + let _ = app_req.emit("open-cloud-file", path); + // Show the window + if let Some(w) = app_req.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + } + } + std::fs::remove_file(&request_file).ok(); + } + } + }); + // Handle .cloud file opened via file association (double-click) let args: Vec = std::env::args().collect(); if args.len() > 1 { diff --git a/clients/desktop/src-tauri/src/sync/config.rs b/clients/desktop/src-tauri/src/sync/config.rs index 4b8f1ed..95e38ec 100644 --- a/clients/desktop/src-tauri/src/sync/config.rs +++ b/clients/desktop/src-tauri/src/sync/config.rs @@ -6,56 +6,71 @@ use std::path::PathBuf; pub struct AppConfig { pub server_url: String, pub username: String, + #[serde(default)] + pub password_b64: String, // base64 encoded (not plaintext in JSON) pub sync_paths: Vec, #[serde(default)] pub auto_start: bool, } impl AppConfig { - /// Get the config file path (OS-specific app data directory) - fn config_path() -> PathBuf { - let config_dir = dirs::config_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("MiniCloud Sync"); - std::fs::create_dir_all(&config_dir).ok(); - config_dir.join("config.json") + /// Get the config directory + fn config_dir() -> PathBuf { + // Windows: %APPDATA%/MiniCloud Sync + // Linux: ~/.config/MiniCloud Sync + // Mac: ~/Library/Application Support/MiniCloud Sync + let base = dirs::config_dir() + .or_else(|| dirs::home_dir().map(|h| h.join(".config"))) + .unwrap_or_else(|| PathBuf::from(".")); + let dir = base.join("MiniCloud Sync"); + std::fs::create_dir_all(&dir).ok(); + dir + } + + fn config_path() -> PathBuf { + Self::config_dir().join("config.json") } - /// Load config from disk pub fn load() -> Self { let path = Self::config_path(); + eprintln!("[Config] Loading from: {}", path.display()); if path.exists() { match std::fs::read_to_string(&path) { Ok(content) => { match serde_json::from_str(&content) { - Ok(config) => return config, - Err(e) => eprintln!("[Config] Parse-Fehler: {}", e), + Ok(config) => { + eprintln!("[Config] Loaded OK"); + return config; + } + Err(e) => eprintln!("[Config] Parse error: {}", e), } } - Err(e) => eprintln!("[Config] Lese-Fehler: {}", e), + Err(e) => eprintln!("[Config] Read error: {}", e), } + } else { + eprintln!("[Config] No config file found"); } Self::default() } - /// Save config to disk pub fn save(&self) -> Result<(), String> { let path = Self::config_path(); + eprintln!("[Config] Saving to: {}", path.display()); let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?; - std::fs::write(&path, json).map_err(|e| format!("Config speichern: {}", e)) + std::fs::write(&path, &json).map_err(|e| format!("Config save: {}", e))?; + eprintln!("[Config] Saved OK"); + Ok(()) } - /// Save password to OS keychain - pub fn save_password(server_url: &str, username: &str, password: &str) -> Result<(), String> { - let service = format!("minicloud-sync-{}", server_url); - let entry = keyring::Entry::new(&service, username).map_err(|e| e.to_string())?; - entry.set_password(password).map_err(|e| format!("Keychain: {}", e)) + pub fn save_password(&mut self, password: &str) { + use base64::Engine; + self.password_b64 = base64::engine::general_purpose::STANDARD.encode(password.as_bytes()); } - /// Load password from OS keychain - pub fn load_password(server_url: &str, username: &str) -> Option { - let service = format!("minicloud-sync-{}", server_url); - let entry = keyring::Entry::new(&service, username).ok()?; - entry.get_password().ok() + pub fn get_password(&self) -> Option { + if self.password_b64.is_empty() { return None; } + use base64::Engine; + let bytes = base64::engine::general_purpose::STANDARD.decode(&self.password_b64).ok()?; + String::from_utf8(bytes).ok() } }