mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-24 07:45:57 +02:00
WebDAV sync: make config per-vault so switching vaults no longer cross-contaminates remotes; migrate existing global config
to the active vault
This commit is contained in:
+68
-15
@@ -43,6 +43,7 @@ pub fn open_vault(app: AppHandle, state: State<'_, AppState>, path: String) -> R
|
|||||||
config.vaults.push(VaultConfig {
|
config.vaults.push(VaultConfig {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
name,
|
name,
|
||||||
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
config.active_vault = Some(path);
|
config.active_vault = Some(path);
|
||||||
@@ -2200,19 +2201,29 @@ pub fn test_ai_connection(app: AppHandle) -> Result<(), String> {
|
|||||||
|
|
||||||
// ── Sync (WebDAV) ──
|
// ── 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<crate::sync::WebdavConfig, String> {
|
fn sync_config_from(config: &AppConfig) -> Result<crate::sync::WebdavConfig, String> {
|
||||||
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());
|
return Err("Sync is not configured".to_string());
|
||||||
}
|
}
|
||||||
let url = config
|
let url = v
|
||||||
.webdav_url
|
.webdav_url
|
||||||
.clone()
|
.clone()
|
||||||
.filter(|u| !u.trim().is_empty())
|
.filter(|u| !u.trim().is_empty())
|
||||||
.ok_or("WebDAV URL is not set")?;
|
.ok_or("WebDAV URL is not set")?;
|
||||||
Ok(crate::sync::WebdavConfig {
|
Ok(crate::sync::WebdavConfig {
|
||||||
url,
|
url,
|
||||||
username: config.webdav_username.clone().unwrap_or_default(),
|
username: v.webdav_username.clone().unwrap_or_default(),
|
||||||
password: config.webdav_password.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,
|
sync_interval_minutes: u32,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut config = state.config.lock().map_err(|e| e.to_string())?;
|
let mut config = state.config.lock().map_err(|e| e.to_string())?;
|
||||||
config.sync_provider = provider.filter(|p| !p.is_empty());
|
let active = config.active_vault.clone().ok_or("No active vault")?;
|
||||||
config.webdav_url = url.filter(|u| !u.trim().is_empty());
|
let v = config
|
||||||
config.webdav_username = username.filter(|u| !u.is_empty());
|
.vaults
|
||||||
config.webdav_password = password.filter(|p| !p.is_empty());
|
.iter_mut()
|
||||||
config.sync_on_open = sync_on_open;
|
.find(|v| v.path == active)
|
||||||
config.sync_on_change = sync_on_change;
|
.ok_or_else(|| "Active vault not found in config".to_string())?;
|
||||||
config.sync_interval_minutes = sync_interval_minutes;
|
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)?;
|
save_app_config(&config)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2298,13 +2315,15 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
use tauri::Emitter;
|
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::<AppState>().syncing.store(false, Ordering::SeqCst);
|
app.state::<AppState>().syncing.store(false, Ordering::SeqCst);
|
||||||
match result {
|
match result {
|
||||||
Ok(summary) => {
|
Ok(summary) => {
|
||||||
let ts = chrono::Utc::now().to_rfc3339();
|
let ts = chrono::Utc::now().to_rfc3339();
|
||||||
if let Ok(mut config) = app.state::<AppState>().config.lock() {
|
if let Ok(mut config) = app.state::<AppState>().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 _ = save_app_config(&config);
|
||||||
}
|
}
|
||||||
let _ = app.emit(
|
let _ = app.emit(
|
||||||
@@ -2424,11 +2443,45 @@ fn app_config_path() -> Result<std::path::PathBuf, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_app_config() -> AppConfig {
|
pub fn load_app_config() -> AppConfig {
|
||||||
app_config_path()
|
let mut config: AppConfig = app_config_path()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|p| std::fs::read_to_string(p).ok())
|
.and_then(|p| std::fs::read_to_string(p).ok())
|
||||||
.and_then(|s| serde_json::from_str(&s).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> {
|
fn save_app_config(config: &AppConfig) -> Result<(), String> {
|
||||||
|
|||||||
+19
-2
@@ -50,10 +50,27 @@ pub struct NoteContent {
|
|||||||
pub raw: String,
|
pub raw: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct VaultConfig {
|
pub struct VaultConfig {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub name: 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<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub webdav_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub webdav_username: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub webdav_password: Option<String>,
|
||||||
|
#[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<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -172,7 +189,7 @@ pub struct AppConfig {
|
|||||||
pub enable_wiki_links: bool,
|
pub enable_wiki_links: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub restore_last_session: bool,
|
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)]
|
#[serde(default)]
|
||||||
pub sync_provider: Option<String>,
|
pub sync_provider: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
notebookOrder,
|
notebookOrder,
|
||||||
noteOrder,
|
noteOrder,
|
||||||
syncState,
|
syncState,
|
||||||
platformIsMobile
|
platformIsMobile,
|
||||||
|
activeVaultConfig,
|
||||||
} from '$lib/stores/app';
|
} from '$lib/stores/app';
|
||||||
import { keybindings, matchAction } from '$lib/keybindings';
|
import { keybindings, matchAction } from '$lib/keybindings';
|
||||||
|
|
||||||
@@ -121,16 +122,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncConfigured(): boolean {
|
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).
|
// Auto-sync on a timer (only when configured and an interval is set).
|
||||||
async function checkScheduledSync() {
|
async function checkScheduledSync() {
|
||||||
const config = get(appConfig);
|
const vc = activeVaultConfig(get(appConfig));
|
||||||
if (config?.sync_provider !== 'webdav') return;
|
if (vc?.sync_provider !== 'webdav') return;
|
||||||
const mins = config.sync_interval_minutes ?? 0;
|
const mins = vc.sync_interval_minutes ?? 0;
|
||||||
if (!mins || get(syncState).running) return;
|
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) {
|
if (Date.now() - last >= mins * 60 * 1000) {
|
||||||
try { await syncNow(); } catch (_) {}
|
try { await syncNow(); } catch (_) {}
|
||||||
}
|
}
|
||||||
@@ -711,12 +712,13 @@
|
|||||||
unlistenSync.push(await listen('sync-done', (event: any) => {
|
unlistenSync.push(await listen('sync-done', (event: any) => {
|
||||||
syncState.set({ running: false, error: null });
|
syncState.set({ running: false, error: null });
|
||||||
const cur = get(appConfig);
|
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' })));
|
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)
|
// 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 (_) {}
|
try { await syncNow(); } catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,8 +728,8 @@
|
|||||||
|
|
||||||
// Auto-sync on note change: a save flips editorDirty true -> false. Debounce a sync.
|
// Auto-sync on note change: a save flips editorDirty true -> false. Debounce a sync.
|
||||||
unsubDirty = editorDirty.subscribe((d) => {
|
unsubDirty = editorDirty.subscribe((d) => {
|
||||||
const config = get(appConfig);
|
const vc = activeVaultConfig(get(appConfig));
|
||||||
if (prevDirty && !d && config?.sync_provider === 'webdav' && config?.sync_on_change) {
|
if (prevDirty && !d && vc?.sync_provider === 'webdav' && vc?.sync_on_change) {
|
||||||
if (onChangeSyncTimer) clearTimeout(onChangeSyncTimer);
|
if (onChangeSyncTimer) clearTimeout(onChangeSyncTimer);
|
||||||
onChangeSyncTimer = setTimeout(() => { if (!get(syncState).running) syncNow().catch(() => {}); }, 15000);
|
onChangeSyncTimer = setTimeout(() => { if (!get(syncState).running) syncNow().catch(() => {}); }, 15000);
|
||||||
}
|
}
|
||||||
@@ -843,7 +845,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $appConfig?.sync_provider === 'webdav'}
|
{#if activeVaultConfig($appConfig)?.sync_provider === 'webdav'}
|
||||||
<button class="mobile-header-btn" class:active={$syncState.running} onclick={triggerSyncNow} disabled={$syncState.running} title={$syncState.error ? `Sync error: ${$syncState.error}` : ($syncState.running ? 'Syncing...' : 'Sync now')}>
|
<button class="mobile-header-btn" class:active={$syncState.running} onclick={triggerSyncNow} disabled={$syncState.running} title={$syncState.error ? `Sync error: ${$syncState.error}` : ($syncState.running ? 'Syncing...' : 'Sync now')}>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:sync-spin={$syncState.running}>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:sync-spin={$syncState.running}>
|
||||||
<path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0115-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 01-15 6.7L3 16"/>
|
<path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0115-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 01-15 6.7L3 16"/>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
NoteEntry,
|
NoteEntry,
|
||||||
NoteContent,
|
NoteContent,
|
||||||
NotebookEntry,
|
NotebookEntry,
|
||||||
|
VaultConfig,
|
||||||
VaultState,
|
VaultState,
|
||||||
ViewMode,
|
ViewMode,
|
||||||
SortMode,
|
SortMode,
|
||||||
@@ -13,6 +14,12 @@ import type {
|
|||||||
|
|
||||||
// App state
|
// App state
|
||||||
export const appConfig = writable<AppConfig | null>(null);
|
export const appConfig = writable<AppConfig | null>(null);
|
||||||
|
|
||||||
|
// The active vault's config entry (where per-vault WebDAV sync settings live).
|
||||||
|
export function activeVaultConfig(c: AppConfig | null): VaultConfig | null {
|
||||||
|
if (!c?.active_vault) return null;
|
||||||
|
return c.vaults.find((v) => v.path === c.active_vault) ?? null;
|
||||||
|
}
|
||||||
export const vaultReady = writable(false);
|
export const vaultReady = writable(false);
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ export interface NoteContent {
|
|||||||
export interface VaultConfig {
|
export interface VaultConfig {
|
||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
// Per-vault WebDAV sync (moved off the global AppConfig).
|
||||||
|
sync_provider?: string | null;
|
||||||
|
webdav_url?: string | null;
|
||||||
|
webdav_username?: string | null;
|
||||||
|
webdav_password?: string | null;
|
||||||
|
sync_on_open?: boolean;
|
||||||
|
sync_on_change?: boolean;
|
||||||
|
sync_interval_minutes?: number;
|
||||||
|
last_sync_time?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomThemeColors {
|
export interface CustomThemeColors {
|
||||||
|
|||||||
Reference in New Issue
Block a user