From a43bf05bf099d14bc16e859d5fe087895d83e552 Mon Sep 17 00:00:00 2001 From: Yuri Karamian Date: Sat, 27 Jun 2026 01:08:02 +0200 Subject: [PATCH] WebDAV sync: make config per-vault so switching vaults no longer cross-contaminates remotes; migrate existing global config to the active vault --- src-tauri/src/commands.rs | 83 +++++++++++++++++++++++------ src-tauri/src/types.rs | 21 +++++++- src/lib/components/AppLayout.svelte | 24 +++++---- src/lib/stores/app.ts | 7 +++ src/lib/types.ts | 9 ++++ 5 files changed, 116 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9185c91..4bdf2d7 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -43,6 +43,7 @@ pub fn open_vault(app: AppHandle, state: State<'_, AppState>, path: String) -> R config.vaults.push(VaultConfig { path: path.clone(), name, + ..Default::default() }); } config.active_vault = Some(path); @@ -2200,19 +2201,29 @@ pub fn test_ai_connection(app: AppHandle) -> Result<(), String> { // ── Sync (WebDAV) ── +fn active_vault_config(config: &AppConfig) -> Result<&VaultConfig, String> { + let active = config.active_vault.as_deref().ok_or("No active vault")?; + config + .vaults + .iter() + .find(|v| v.path == active) + .ok_or_else(|| "Active vault not found in config".to_string()) +} + fn sync_config_from(config: &AppConfig) -> Result { - if config.sync_provider.as_deref() != Some("webdav") { + let v = active_vault_config(config)?; + if v.sync_provider.as_deref() != Some("webdav") { return Err("Sync is not configured".to_string()); } - let url = config + let url = v .webdav_url .clone() .filter(|u| !u.trim().is_empty()) .ok_or("WebDAV URL is not set")?; Ok(crate::sync::WebdavConfig { url, - username: config.webdav_username.clone().unwrap_or_default(), - password: config.webdav_password.clone().unwrap_or_default(), + username: v.webdav_username.clone().unwrap_or_default(), + password: v.webdav_password.clone().unwrap_or_default(), }) } @@ -2228,13 +2239,19 @@ pub fn set_sync_settings( sync_interval_minutes: u32, ) -> Result<(), String> { let mut config = state.config.lock().map_err(|e| e.to_string())?; - config.sync_provider = provider.filter(|p| !p.is_empty()); - config.webdav_url = url.filter(|u| !u.trim().is_empty()); - config.webdav_username = username.filter(|u| !u.is_empty()); - config.webdav_password = password.filter(|p| !p.is_empty()); - config.sync_on_open = sync_on_open; - config.sync_on_change = sync_on_change; - config.sync_interval_minutes = sync_interval_minutes; + let active = config.active_vault.clone().ok_or("No active vault")?; + let v = config + .vaults + .iter_mut() + .find(|v| v.path == active) + .ok_or_else(|| "Active vault not found in config".to_string())?; + v.sync_provider = provider.filter(|p| !p.is_empty()); + v.webdav_url = url.filter(|u| !u.trim().is_empty()); + v.webdav_username = username.filter(|u| !u.is_empty()); + v.webdav_password = password.filter(|p| !p.is_empty()); + v.sync_on_open = sync_on_open; + v.sync_on_change = sync_on_change; + v.sync_interval_minutes = sync_interval_minutes; save_app_config(&config)?; Ok(()) } @@ -2298,13 +2315,15 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> { }; std::thread::spawn(move || { use tauri::Emitter; - let result = crate::sync::run_sync(app.clone(), vault, cfg); + let result = crate::sync::run_sync(app.clone(), vault.clone(), cfg); app.state::().syncing.store(false, Ordering::SeqCst); match result { Ok(summary) => { let ts = chrono::Utc::now().to_rfc3339(); if let Ok(mut config) = app.state::().config.lock() { - config.last_sync_time = Some(ts.clone()); + if let Some(v) = config.vaults.iter_mut().find(|v| v.path == vault) { + v.last_sync_time = Some(ts.clone()); + } let _ = save_app_config(&config); } let _ = app.emit( @@ -2424,11 +2443,45 @@ fn app_config_path() -> Result { } pub fn load_app_config() -> AppConfig { - app_config_path() + let mut config: AppConfig = app_config_path() .ok() .and_then(|p| std::fs::read_to_string(p).ok()) .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default() + .unwrap_or_default(); + if migrate_global_sync_to_vault(&mut config) { + let _ = save_app_config(&config); + } + config +} + +// One-time migration: WebDAV sync moved from global AppConfig to per-vault VaultConfig. +// Copy the old global settings into the active vault's config if it has none yet. Idempotent. +fn migrate_global_sync_to_vault(config: &mut AppConfig) -> bool { + if config.sync_provider.is_none() && config.webdav_url.is_none() { + return false; + } + let Some(active) = config.active_vault.clone() else { return false; }; + let g_provider = config.sync_provider.clone(); + let g_url = config.webdav_url.clone(); + let g_user = config.webdav_username.clone(); + let g_pass = config.webdav_password.clone(); + let g_on_open = config.sync_on_open; + let g_on_change = config.sync_on_change; + let g_interval = config.sync_interval_minutes; + let g_last = config.last_sync_time.clone(); + let Some(v) = config.vaults.iter_mut().find(|v| v.path == active) else { return false; }; + if v.sync_provider.is_some() || v.webdav_url.is_some() { + return false; // already migrated / has its own config + } + v.sync_provider = g_provider; + v.webdav_url = g_url; + v.webdav_username = g_user; + v.webdav_password = g_pass; + v.sync_on_open = g_on_open; + v.sync_on_change = g_on_change; + v.sync_interval_minutes = g_interval; + v.last_sync_time = g_last; + true } fn save_app_config(config: &AppConfig) -> Result<(), String> { diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index d0cabaf..9425592 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -50,10 +50,27 @@ pub struct NoteContent { pub raw: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct VaultConfig { pub path: String, pub name: String, + // Per-vault WebDAV sync (was previously global on AppConfig; those fields are kept deprecated for migration). + #[serde(default)] + pub sync_provider: Option, + #[serde(default)] + pub webdav_url: Option, + #[serde(default)] + pub webdav_username: Option, + #[serde(default)] + pub webdav_password: Option, + #[serde(default)] + pub sync_on_open: bool, + #[serde(default)] + pub sync_on_change: bool, + #[serde(default)] + pub sync_interval_minutes: u32, + #[serde(default)] + pub last_sync_time: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -172,7 +189,7 @@ pub struct AppConfig { pub enable_wiki_links: bool, #[serde(default)] pub restore_last_session: bool, - // WebDAV sync (opt-in; all off by default). Endpoint is fully user-configured. + // DEPRECATED: WebDAV sync moved to per-vault VaultConfig. Kept for one release to migrate old configs. #[serde(default)] pub sync_provider: Option, #[serde(default)] diff --git a/src/lib/components/AppLayout.svelte b/src/lib/components/AppLayout.svelte index bdd36ae..fec533f 100644 --- a/src/lib/components/AppLayout.svelte +++ b/src/lib/components/AppLayout.svelte @@ -53,7 +53,8 @@ notebookOrder, noteOrder, syncState, - platformIsMobile + platformIsMobile, + activeVaultConfig, } from '$lib/stores/app'; import { keybindings, matchAction } from '$lib/keybindings'; @@ -121,16 +122,16 @@ } function syncConfigured(): boolean { - return get(appConfig)?.sync_provider === 'webdav'; + return activeVaultConfig(get(appConfig))?.sync_provider === 'webdav'; } // Auto-sync on a timer (only when configured and an interval is set). async function checkScheduledSync() { - const config = get(appConfig); - if (config?.sync_provider !== 'webdav') return; - const mins = config.sync_interval_minutes ?? 0; + const vc = activeVaultConfig(get(appConfig)); + if (vc?.sync_provider !== 'webdav') return; + const mins = vc.sync_interval_minutes ?? 0; if (!mins || get(syncState).running) return; - const last = config.last_sync_time ? new Date(config.last_sync_time).getTime() : 0; + const last = vc.last_sync_time ? new Date(vc.last_sync_time).getTime() : 0; if (Date.now() - last >= mins * 60 * 1000) { try { await syncNow(); } catch (_) {} } @@ -711,12 +712,13 @@ unlistenSync.push(await listen('sync-done', (event: any) => { syncState.set({ running: false, error: null }); const cur = get(appConfig); - if (cur && event.payload?.last_sync_time) appConfig.set({ ...cur, last_sync_time: event.payload.last_sync_time }); + const ts = event.payload?.last_sync_time; + if (cur && ts) appConfig.set({ ...cur, vaults: cur.vaults.map((v) => v.path === cur.active_vault ? { ...v, last_sync_time: ts } : v) }); })); unlistenSync.push(await listen('sync-error', (event: any) => syncState.set({ running: false, error: event.payload?.error ?? 'Sync failed' }))); // Sync when the vault opens (if enabled) - if (syncConfigured() && get(appConfig)?.sync_on_open) { + if (syncConfigured() && activeVaultConfig(get(appConfig))?.sync_on_open) { try { await syncNow(); } catch (_) {} } @@ -726,8 +728,8 @@ // Auto-sync on note change: a save flips editorDirty true -> false. Debounce a sync. unsubDirty = editorDirty.subscribe((d) => { - const config = get(appConfig); - if (prevDirty && !d && config?.sync_provider === 'webdav' && config?.sync_on_change) { + const vc = activeVaultConfig(get(appConfig)); + if (prevDirty && !d && vc?.sync_provider === 'webdav' && vc?.sync_on_change) { if (onChangeSyncTimer) clearTimeout(onChangeSyncTimer); onChangeSyncTimer = setTimeout(() => { if (!get(syncState).running) syncNow().catch(() => {}); }, 15000); } @@ -843,7 +845,7 @@ {/if} - {#if $appConfig?.sync_provider === 'webdav'} + {#if activeVaultConfig($appConfig)?.sync_provider === 'webdav'}