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) <noreply@anthropic.com>
This commit is contained in:
parent
c354682905
commit
4cc8de8a1a
|
|
@ -26,5 +26,5 @@ sha2 = "0.10"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
rusqlite = { version = "0.34", features = ["bundled"] }
|
rusqlite = { version = "0.34", features = ["bundled"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
keyring = "3"
|
base64 = "0.22"
|
||||||
open = "5"
|
open = "5"
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,11 @@ async fn login(
|
||||||
*state.api.lock().unwrap() = Some(api);
|
*state.api.lock().unwrap() = Some(api);
|
||||||
*state.username.lock().unwrap() = Some(username.clone());
|
*state.username.lock().unwrap() = Some(username.clone());
|
||||||
|
|
||||||
// Save credentials
|
// Save credentials to config file
|
||||||
let _ = AppConfig::save_password(&server_url, &username, &password);
|
|
||||||
let mut config = AppConfig::load();
|
let mut config = AppConfig::load();
|
||||||
config.server_url = server_url;
|
config.server_url = server_url;
|
||||||
config.username = username;
|
config.username = username;
|
||||||
|
config.save_password(&password);
|
||||||
let _ = config.save();
|
let _ = config.save();
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
|
|
@ -57,11 +57,9 @@ async fn login(
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn load_saved_config() -> Result<serde_json::Value, String> {
|
fn load_saved_config() -> Result<serde_json::Value, String> {
|
||||||
let config = AppConfig::load();
|
let config = AppConfig::load();
|
||||||
let has_credentials = if !config.server_url.is_empty() && !config.username.is_empty() {
|
let has_credentials = !config.server_url.is_empty()
|
||||||
AppConfig::load_password(&config.server_url, &config.username).is_some()
|
&& !config.username.is_empty()
|
||||||
} else {
|
&& config.get_password().is_some();
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"server_url": config.server_url,
|
"server_url": config.server_url,
|
||||||
|
|
@ -79,8 +77,8 @@ async fn auto_login(state: State<'_, AppState>) -> Result<serde_json::Value, Str
|
||||||
return Err("Keine gespeicherten Zugangsdaten".to_string());
|
return Err("Keine gespeicherten Zugangsdaten".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let password = AppConfig::load_password(&config.server_url, &config.username)
|
let password = config.get_password()
|
||||||
.ok_or("Passwort nicht im Keychain gefunden")?;
|
.ok_or("Passwort nicht gespeichert")?;
|
||||||
|
|
||||||
let mut api = MiniCloudApi::new(&config.server_url);
|
let mut api = MiniCloudApi::new(&config.server_url);
|
||||||
let result = api.login(&config.username, &password).await?;
|
let result = api.login(&config.username, &password).await?;
|
||||||
|
|
@ -527,8 +525,36 @@ fn start_background_sync(
|
||||||
|
|
||||||
// --- App Setup ---
|
// --- App Setup ---
|
||||||
|
|
||||||
|
/// Check if another instance is running. If yes, pass the .cloud file to it and exit.
|
||||||
|
fn handle_single_instance() {
|
||||||
|
let config_dir = dirs::config_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
.join("MiniCloud Sync");
|
||||||
|
std::fs::create_dir_all(&config_dir).ok();
|
||||||
|
|
||||||
|
let lock_file = config_dir.join("instance.lock");
|
||||||
|
let args: Vec<String> = 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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
|
handle_single_instance();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
|
|
@ -558,6 +584,32 @@ pub fn run() {
|
||||||
// Use window icon for tray (set via tauri.conf.json)
|
// Use window icon for tray (set via tauri.conf.json)
|
||||||
let icon = app.default_window_icon().cloned();
|
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)
|
// Handle .cloud file opened via file association (double-click)
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
if args.len() > 1 {
|
if args.len() > 1 {
|
||||||
|
|
|
||||||
|
|
@ -6,56 +6,71 @@ use std::path::PathBuf;
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub server_url: String,
|
pub server_url: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub password_b64: String, // base64 encoded (not plaintext in JSON)
|
||||||
pub sync_paths: Vec<SyncPath>,
|
pub sync_paths: Vec<SyncPath>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auto_start: bool,
|
pub auto_start: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
/// Get the config file path (OS-specific app data directory)
|
/// Get the config directory
|
||||||
fn config_path() -> PathBuf {
|
fn config_dir() -> PathBuf {
|
||||||
let config_dir = dirs::config_dir()
|
// Windows: %APPDATA%/MiniCloud Sync
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
// Linux: ~/.config/MiniCloud Sync
|
||||||
.join("MiniCloud Sync");
|
// Mac: ~/Library/Application Support/MiniCloud Sync
|
||||||
std::fs::create_dir_all(&config_dir).ok();
|
let base = dirs::config_dir()
|
||||||
config_dir.join("config.json")
|
.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 {
|
pub fn load() -> Self {
|
||||||
let path = Self::config_path();
|
let path = Self::config_path();
|
||||||
|
eprintln!("[Config] Loading from: {}", path.display());
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
match std::fs::read_to_string(&path) {
|
match std::fs::read_to_string(&path) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
match serde_json::from_str(&content) {
|
match serde_json::from_str(&content) {
|
||||||
Ok(config) => return config,
|
Ok(config) => {
|
||||||
Err(e) => eprintln!("[Config] Parse-Fehler: {}", e),
|
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()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save config to disk
|
|
||||||
pub fn save(&self) -> Result<(), String> {
|
pub fn save(&self) -> Result<(), String> {
|
||||||
let path = Self::config_path();
|
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())?;
|
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(&mut self, password: &str) {
|
||||||
pub fn save_password(server_url: &str, username: &str, password: &str) -> Result<(), String> {
|
use base64::Engine;
|
||||||
let service = format!("minicloud-sync-{}", server_url);
|
self.password_b64 = base64::engine::general_purpose::STANDARD.encode(password.as_bytes());
|
||||||
let entry = keyring::Entry::new(&service, username).map_err(|e| e.to_string())?;
|
|
||||||
entry.set_password(password).map_err(|e| format!("Keychain: {}", e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load password from OS keychain
|
pub fn get_password(&self) -> Option<String> {
|
||||||
pub fn load_password(server_url: &str, username: &str) -> Option<String> {
|
if self.password_b64.is_empty() { return None; }
|
||||||
let service = format!("minicloud-sync-{}", server_url);
|
use base64::Engine;
|
||||||
let entry = keyring::Entry::new(&service, username).ok()?;
|
let bytes = base64::engine::general_purpose::STANDARD.decode(&self.password_b64).ok()?;
|
||||||
entry.get_password().ok()
|
String::from_utf8(bytes).ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue