feat(sync): WebDAV vault sync
All checks were successful
CI / Windows build (push) Successful in 12m32s
All checks were successful
CI / Windows build (push) Successful in 12m32s
Two-way sync of the vault folder to a user-configured WebDAV server, so annotations (which travel with the file) sync with the file. - WebDavSyncService.syncNow: per-file decision — local-only uploads, remote-only downloads, and when BOTH sides changed since the last sync it keeps the loser as <file>.conflict-<mtime> on both sides (last-write-wins by mtime) so no data is ever lost. Creates dirs as needed; deletes are conservative. - The decision logic is pure and unit-tested against a fake WebDAV client; the real client is a thin http adapter (no dio dependency). - Settings: WebDAV URL / user / password / remote folder, Test connection, Sync now (with status + last-synced), and an auto-sync toggle (default OFF). Real server round-trips are device/server-validated. Credentials are in SharedPreferences for now (TODO secure-storage). analyze clean, 432 tests.
This commit is contained in:
@@ -14,6 +14,7 @@ import 'screens/home_screen.dart';
|
||||
import 'screens/vault_setup_screen.dart';
|
||||
import 'services/database_service.dart';
|
||||
import 'services/vault_service.dart';
|
||||
import 'services/webdav_sync_service.dart';
|
||||
import 'storage/sqlite_to_sidecar_migrator.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
@@ -141,7 +142,34 @@ class _VaultGateState extends State<VaultGate> {
|
||||
_hadStoredPath = (vault.vaultRoot?.isNotEmpty ?? false);
|
||||
_loading = false;
|
||||
});
|
||||
if (valid) await _maybeMigrate(vault);
|
||||
if (valid) {
|
||||
await _maybeMigrate(vault);
|
||||
_maybeAutoSync(vault); // fire-and-forget; never blocks the UI
|
||||
}
|
||||
}
|
||||
|
||||
/// Optionally kick off a WebDAV sync on launch when the user has enabled
|
||||
/// auto-sync (default OFF). Deliberately NON-blocking and failure-tolerant: a
|
||||
/// bad config or offline server must never delay or crash startup. Results
|
||||
/// are surfaced in Settings (last-synced time) rather than interrupting here.
|
||||
Future<void> _maybeAutoSync(VaultService vault) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final sync = WebDavSyncService(prefs);
|
||||
final config = sync.config;
|
||||
if (!config.autoSync || !config.isConfigured) return;
|
||||
final root = vault.vaultRoot;
|
||||
if (root == null || root.isEmpty) return;
|
||||
final client = sync.buildClient();
|
||||
if (client == null) return;
|
||||
try {
|
||||
await sync.syncNow(vaultRoot: root, client: client);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
} catch (_) {
|
||||
// Auto-sync is best-effort; swallow everything so launch is unaffected.
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the one-time SQLite→sidecar migration ONCE per vault (Phase 5, §B).
|
||||
|
||||
Reference in New Issue
Block a user