From 339a4bc9c1eda5fca46c32c85adbdf6dcca2eb15 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 6 Jul 2026 10:31:20 +0000 Subject: [PATCH] feat: add WebDAV journal storage support --- docs/settings.md | 8 +- package.json | 5 + src/apps/cli/commands/runCommand.unit.spec.ts | 22 ++- src/lib | 2 +- src/modules/core/ModuleReplicatorCouchDB.ts | 6 +- src/modules/core/ModuleReplicatorMinIO.ts | 4 +- .../ObsidianLiveSyncSettingTab.ts | 16 +- .../SettingDialogue/PaneMaintenance.ts | 10 +- .../SettingDialogue/PaneRemoteConfig.ts | 38 ++++- .../PaneRemoteConfig.unit.spec.ts | 12 ++ .../SettingDialogue/remoteConfigBuffer.ts | 8 +- .../remoteConfigBuffer.unit.spec.ts | 23 ++- .../SettingDialogue/remoteConfigDisplay.ts | 29 ++++ src/modules/features/SetupManager.ts | 33 ++++ .../SetupWizard/dialogs/SetupRemote.svelte | 14 +- .../dialogs/SetupRemoteWebDAV.svelte | 160 ++++++++++++++++++ .../SetupWizard/dialogs/setupDialogTypes.ts | 11 +- src/modules/services/ObsidianAPIService.ts | 12 +- .../services/ObsidianAPIService.unit.spec.ts | 63 +++++++ test/shell/webdav-init.sh | 4 + test/shell/webdav-start.sh | 15 ++ test/shell/webdav-stop.sh | 3 + updates.md | 7 + 23 files changed, 474 insertions(+), 31 deletions(-) create mode 100644 src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts create mode 100644 src/modules/features/SettingDialogue/remoteConfigDisplay.ts create mode 100644 src/modules/features/SetupWizard/dialogs/SetupRemoteWebDAV.svelte create mode 100644 src/modules/services/ObsidianAPIService.unit.spec.ts create mode 100755 test/shell/webdav-init.sh create mode 100755 test/shell/webdav-start.sh create mode 100755 test/shell/webdav-stop.sh diff --git a/docs/settings.md b/docs/settings.md index 32c3e91..79de8ee 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -153,7 +153,7 @@ Show verbose log. Please enable when you report the logs Self-hosted LiveSync supports multiple remote connection profiles under **Remote Server** -> **Remote Databases**. This allows you to save and switch between multiple databases or bucket configurations in a single vault. - **➕ Add new connection**: Create a new connection profile by launching the setup dialogue. -- **📥 Import connection**: Paste a connection string (e.g., `sls+https://...`, `sls+s3://...`, `sls+p2p://...`) to import a remote configuration profile. +- **📥 Import connection**: Paste a connection string (e.g., `sls+https://...`, `sls+s3://...`, `sls+webdav://...`, `sls+p2p://...`) to import a remote configuration profile. - **🔧 Configure**: Open the setup dialogue to edit settings for the selected connection profile. - **✅ Activate**: Select and activate this profile as the current active remote. - **🗑️ Delete**: Remove this connection profile from the list. @@ -164,6 +164,12 @@ Setting key: remoteType The active remote server type. This is automatically projected to the legacy configuration when you activate a connection profile. +#### WebDAV Connection URI (Experimental) + +Setting key: webDAVactiveConnectionURI + +The active WebDAV connection URI used by experimental Journal synchronisation storage. This is configured in the **WebDAV Configuration** setup dialogue as a single `sls+webdav://...` URI. Optional query parameters include `prefix`, `useProxy=true`, and `insecure=true` for local HTTP testing. This feature is still a proof of concept for pluggable Journal storage backends and should be treated as experimental. + ### 2. Notification #### Notify when the estimated remote storage size exceeds on start up diff --git a/package.json b/package.json index 6fd1a73..9d65b8e 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,11 @@ "test:docker-s3:start": "npm run test:docker-s3:up && sleep 3 && npm run test:docker-s3:init", "test:docker-s3:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/minio-stop.sh", "test:docker-s3:stop": "npm run test:docker-s3:down", + "test:docker-webdav:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/webdav-start.sh", + "test:docker-webdav:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/webdav-init.sh", + "test:docker-webdav:start": "npm run test:docker-webdav:up && sleep 3 && npm run test:docker-webdav:init", + "test:docker-webdav:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/webdav-stop.sh", + "test:docker-webdav:stop": "npm run test:docker-webdav:down", "test:docker-p2p:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-start.sh", "test:docker-p2p:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-init.sh", "test:docker-p2p:start": "npm run test:docker-p2p:up && sleep 3 && npm run test:docker-p2p:init", diff --git a/src/apps/cli/commands/runCommand.unit.spec.ts b/src/apps/cli/commands/runCommand.unit.spec.ts index 3d02951..72fb666 100644 --- a/src/apps/cli/commands/runCommand.unit.spec.ts +++ b/src/apps/cli/commands/runCommand.unit.spec.ts @@ -4,7 +4,7 @@ import * as os from "os"; import * as processSetting from "@lib/API/processSetting"; import { ConnectionStringParser } from "@lib/common/ConnectionString"; import { configURIBase } from "@lib/common/models/shared.const"; -import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P } from "@lib/common/types"; +import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, REMOTE_WEBDAV } from "@lib/common/types"; import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { runCommand } from "./runCommand"; import type { CLIOptions } from "./types"; @@ -205,6 +205,22 @@ const protocolFixtures: ProtocolFixture[] = [ expect(settings.P2P_AppID).toBe("self-hosted-livesync"); }, }, + { + protocol: "webdav", + connectionString: ConnectionStringParser.serialize({ + type: "webdav", + settings: { + webDAVactiveConnectionURI: + "sls+webdav://user:pass@webdav.example.com/dav?prefix=vault%2F&headers=x-test%3A1&useProxy=true", + }, + }), + assertProjectedFields: (settings) => { + expect(settings.remoteType).toBe(REMOTE_WEBDAV); + expect(settings.webDAVactiveConnectionURI).toBe( + "sls+webdav://user:pass@webdav.example.com/dav?prefix=vault%2F&headers=x-test%3A1&useProxy=true" + ); + }, + }, ]; describe("runCommand abnormal cases", () => { @@ -564,6 +580,10 @@ describe("runCommand abnormal cases", () => { "p2p", "sls+p2p://room-abc?passphrase=pass-123&relays=wss%3A%2F%2Frelay.example&appId=self-hosted-livesync", ] as const, + [ + "webdav", + "sls+webdav://user:pass@webdav.example.com/dav?prefix=vault%2F&headers=x-test%3A1&useProxy=true", + ] as const, ])("remote command round-trip works for %s", async (_protocol, initialConnStr) => { const core = createCoreMock(); diff --git a/src/lib b/src/lib index 87dc724..2ee5d20 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 87dc724c9d74962d63173bf4da3d1aa74fd0c7d4 +Subproject commit 2ee5d2055ac72ce1606b588d1b1ffce061f02454 diff --git a/src/modules/core/ModuleReplicatorCouchDB.ts b/src/modules/core/ModuleReplicatorCouchDB.ts index 680fc34..1460da0 100644 --- a/src/modules/core/ModuleReplicatorCouchDB.ts +++ b/src/modules/core/ModuleReplicatorCouchDB.ts @@ -1,5 +1,5 @@ import { fireAndForget } from "octagonal-wheels/promises"; -import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "@lib/common/types"; +import { REMOTE_P2P, isJournalRemoteType, type RemoteDBSettings } from "@lib/common/types"; import { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator"; import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; import { AbstractModule } from "@/modules/AbstractModule"; @@ -9,7 +9,7 @@ export class ModuleReplicatorCouchDB extends AbstractModule { _anyNewReplicator(settingOverride: Partial = {}): Promise { const settings = { ...this.settings, ...settingOverride }; // If new remote types were added, add them here. Do not use `REMOTE_COUCHDB` directly for the safety valve. - if (settings.remoteType == REMOTE_MINIO || settings.remoteType == REMOTE_P2P) { + if (isJournalRemoteType(settings.remoteType) || settings.remoteType == REMOTE_P2P) { return Promise.resolve(false); } return Promise.resolve(new LiveSyncCouchDBReplicator(this.core)); @@ -17,7 +17,7 @@ export class ModuleReplicatorCouchDB extends AbstractModule { _everyAfterResumeProcess(): Promise { if (this.services.appLifecycle.isSuspended()) return Promise.resolve(true); if (!this.services.appLifecycle.isReady()) return Promise.resolve(true); - if (this.settings.remoteType != REMOTE_MINIO && this.settings.remoteType != REMOTE_P2P) { + if (!isJournalRemoteType(this.settings.remoteType) && this.settings.remoteType != REMOTE_P2P) { const LiveSyncEnabled = this.settings.liveSync; const continuous = LiveSyncEnabled; const eventualOnStart = !LiveSyncEnabled && this.settings.syncOnStart; diff --git a/src/modules/core/ModuleReplicatorMinIO.ts b/src/modules/core/ModuleReplicatorMinIO.ts index 1d552f0..413bf43 100644 --- a/src/modules/core/ModuleReplicatorMinIO.ts +++ b/src/modules/core/ModuleReplicatorMinIO.ts @@ -1,4 +1,4 @@ -import { REMOTE_MINIO, type RemoteDBSettings } from "@lib/common/types"; +import { isJournalRemoteType, type RemoteDBSettings } from "@lib/common/types"; import { LiveSyncJournalReplicator } from "@lib/replication/journal/LiveSyncJournalReplicator"; import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; import type { LiveSyncCore } from "@/main"; @@ -7,7 +7,7 @@ import { AbstractModule } from "@/modules/AbstractModule"; export class ModuleReplicatorMinIO extends AbstractModule { _anyNewReplicator(settingOverride: Partial = {}): Promise { const settings = { ...this.settings, ...settingOverride }; - if (settings.remoteType == REMOTE_MINIO) { + if (isJournalRemoteType(settings.remoteType)) { return Promise.resolve(new LiveSyncJournalReplicator(this.core)); } return Promise.resolve(false); diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index c6b31b2..5582b26 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -12,6 +12,7 @@ import { LEVEL_ADVANCED, LEVEL_EDGE_CASE, REMOTE_P2P, + isJournalRemoteType, } from "@lib/common/types.ts"; import { delay, isObjectDifferent, sizeToHumanReadable } from "@lib/common/utils.ts"; import { versionNumberString2Number } from "@lib/string_and_binary/convert.ts"; @@ -64,7 +65,7 @@ import { panePatches } from "./PanePatches.ts"; import { paneMaintenance } from "./PaneMaintenance.ts"; import { compatGlobal } from "@lib/common/coreEnvFunctions.ts"; import { JournalSyncCore } from "@lib/replication/journal/JournalSyncCore.js"; -import { MinioStorageAdapter } from "@lib/replication/journal/objectstore/MinioStorageAdapter.js"; +import { createJournalStorageAdapter } from "@lib/replication/journal/objectstore/JournalStorageAdapterFactory.js"; // For creating a document // const toc = new Set(); @@ -530,6 +531,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { ({ visibility: this.isConfiguredAs("remoteType", REMOTE_MINIO), }) as OnUpdateResult; + onlyOnJournalSync = () => + ({ + visibility: isJournalRemoteType(this.editingSettings.remoteType), + }) as OnUpdateResult; onlyOnOnlyP2P = () => ({ visibility: this.isConfiguredAs("remoteType", REMOTE_P2P), @@ -537,11 +542,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { onlyOnCouchDBOrMinIO = () => ({ visibility: - this.isConfiguredAs("remoteType", REMOTE_COUCHDB) || this.isConfiguredAs("remoteType", REMOTE_MINIO), + this.isConfiguredAs("remoteType", REMOTE_COUCHDB) || + isJournalRemoteType(this.editingSettings.remoteType), }) as OnUpdateResult; // E2EE Function checkWorkingPassphrase = async (): Promise => { - if (this.editingSettings.remoteType == REMOTE_MINIO) return true; + if (isJournalRemoteType(this.editingSettings.remoteType)) { + return true; + } const settingForCheck: RemoteDBSettings = { ...this.editingSettings, @@ -856,7 +864,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { this.core.settings, this.core.simpleStore, this.core, - new MinioStorageAdapter(this.core.settings, this.core) + createJournalStorageAdapter(this.core.settings, this.core) ); } async resetRemoteBucket() { diff --git a/src/modules/features/SettingDialogue/PaneMaintenance.ts b/src/modules/features/SettingDialogue/PaneMaintenance.ts index d069588..17e3fee 100644 --- a/src/modules/features/SettingDialogue/PaneMaintenance.ts +++ b/src/modules/features/SettingDialogue/PaneMaintenance.ts @@ -163,7 +163,7 @@ export function paneMaintenance( Logger(`Journal received history has been cleared.`, LOG_LEVEL_NOTICE); }) ) - .addOnUpdate(this.onlyOnMinIO); + .addOnUpdate(this.onlyOnJournalSync); new Setting(paneEl) .setName("Reset journal sent history") @@ -185,7 +185,7 @@ export function paneMaintenance( Logger(`Journal sent history has been cleared.`, LOG_LEVEL_NOTICE); }) ) - .addOnUpdate(this.onlyOnMinIO); + .addOnUpdate(this.onlyOnJournalSync); }); void addPanel(paneEl, "Garbage Collection V3 (Beta)", (e) => e, this.onlyOnP2POrCouchDB).then((paneEl) => { new Setting(paneEl) @@ -336,7 +336,7 @@ export function paneMaintenance( Logger(`Journal exchange history has been cleared.`, LOG_LEVEL_NOTICE); }) ) - .addOnUpdate(this.onlyOnMinIO); + .addOnUpdate(this.onlyOnJournalSync); new Setting(paneEl) .setName("Purge all journal counter") @@ -351,7 +351,7 @@ export function paneMaintenance( Logger(`Journal download/upload cache has been cleared.`, LOG_LEVEL_NOTICE); }) ) - .addOnUpdate(this.onlyOnMinIO); + .addOnUpdate(this.onlyOnJournalSync); new Setting(paneEl) .setName("Fresh Start Wipe") @@ -374,7 +374,7 @@ export function paneMaintenance( Logger(`Deleted all data on remote server`, LOG_LEVEL_NOTICE); }) ) - .addOnUpdate(this.onlyOnMinIO); + .addOnUpdate(this.onlyOnJournalSync); }); void addPanel(paneEl, "Reset").then((paneEl) => { diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts index bdc7734..3857192 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -2,6 +2,7 @@ import { REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, + REMOTE_WEBDAV, DEFAULT_SETTINGS, LOG_LEVEL_NOTICE, type ObsidianLiveSyncSettings, @@ -33,7 +34,9 @@ import SetupRemote from "@/modules/features/SetupWizard/dialogs/SetupRemote.svel import SetupRemoteCouchDB from "@/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte"; import SetupRemoteBucket from "@/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte"; import SetupRemoteP2P from "@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte"; +import SetupRemoteWebDAV from "@/modules/features/SetupWizard/dialogs/SetupRemoteWebDAV.svelte"; import { syncActivatedRemoteSettings } from "./remoteConfigBuffer.ts"; +import { getRemoteConfigurationDescription } from "./remoteConfigDisplay.ts"; function getSettingsFromEditingSettings(editingSettings: AllSettings): ObsidianLiveSyncSettings { const workObj = { ...editingSettings } as ObsidianLiveSyncSettings; @@ -69,6 +72,9 @@ function serializeRemoteConfiguration(settings: ObsidianLiveSyncSettings): strin if (settings.remoteType === REMOTE_P2P) { return ConnectionStringParser.serialize({ type: "p2p", settings }); } + if (settings.remoteType === REMOTE_WEBDAV) { + return ConnectionStringParser.serialize({ type: "webdav", settings }); + } return ConnectionStringParser.serialize({ type: "couchdb", settings }); } @@ -92,6 +98,14 @@ function suggestRemoteConfigurationName(parsed: RemoteConfigurationResult): stri if (parsed.type === "s3") { return `S3 ${parsed.settings.bucket || parsed.settings.endpoint}`; } + if (parsed.type === "webdav") { + try { + const url = new URL(parsed.settings.webDAVactiveConnectionURI.replace(/^sls\+webdav:/, "https:")); + return `WebDAV ${url.host}`; + } catch { + return "Imported WebDAV"; + } + } return `P2P ${parsed.settings.P2P_roomID || "Remote"}`; } @@ -198,7 +212,7 @@ export function paneRemoteConfig( }; const runRemoteSetup = async ( baseSettings: ObsidianLiveSyncSettings, - remoteType?: typeof REMOTE_COUCHDB | typeof REMOTE_MINIO | typeof REMOTE_P2P + remoteType?: typeof REMOTE_COUCHDB | typeof REMOTE_MINIO | typeof REMOTE_P2P | typeof REMOTE_WEBDAV ): Promise => { const setupManager = this.core.getModule(SetupManager); const dialogManager = setupManager.dialogManager; @@ -210,7 +224,13 @@ export function paneRemoteConfig( return false; } targetRemoteType = - method === "bucket" ? REMOTE_MINIO : method === "p2p" ? REMOTE_P2P : REMOTE_COUCHDB; + method === "bucket" + ? REMOTE_MINIO + : method === "p2p" + ? REMOTE_P2P + : method === "webdav" + ? REMOTE_WEBDAV + : REMOTE_COUCHDB; } if (targetRemoteType === REMOTE_MINIO) { @@ -229,6 +249,14 @@ export function paneRemoteConfig( return { ...baseSettings, ...p2pConf, remoteType: REMOTE_P2P }; } + if (targetRemoteType === REMOTE_WEBDAV) { + const webDAVConf = await dialogManager.openWithExplicitCancel(SetupRemoteWebDAV, baseSettings); + if (webDAVConf === "cancelled" || typeof webDAVConf !== "object") { + return false; + } + return { ...baseSettings, ...webDAVConf, remoteType: REMOTE_WEBDAV }; + } + const couchConf = await dialogManager.openWithExplicitCancel(SetupRemoteCouchDB, baseSettings); if (couchConf === "cancelled" || typeof couchConf !== "object") { return false; @@ -331,7 +359,7 @@ export function paneRemoteConfig( for (const config of Object.values(configs)) { const row = new Setting(listContainer) .setName(config.name) - .setDesc(config.uri.split("@").pop() || ""); // Show host part for privacy + .setDesc(getRemoteConfigurationDescription(config.uri)); if (config.id === this.editingSettings.activeConfigurationId) { row.nameEl.addClass("sls-active-remote-name"); @@ -356,6 +384,8 @@ export function paneRemoteConfig( workSettings.remoteType = REMOTE_COUCHDB; } else if (parsed.type === "s3") { workSettings.remoteType = REMOTE_MINIO; + } else if (parsed.type === "webdav") { + workSettings.remoteType = REMOTE_WEBDAV; } else { workSettings.remoteType = REMOTE_P2P; } @@ -466,6 +496,8 @@ export function paneRemoteConfig( workSettings.remoteType = REMOTE_COUCHDB; } else if (parsed.type === "s3") { workSettings.remoteType = REMOTE_MINIO; + } else if (parsed.type === "webdav") { + workSettings.remoteType = REMOTE_WEBDAV; } else { workSettings.remoteType = REMOTE_P2P; } diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts new file mode 100644 index 0000000..3ba76c9 --- /dev/null +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.unit.spec.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { getRemoteConfigurationDescription } from "./remoteConfigDisplay"; + +describe("getRemoteConfigurationDescription", () => { + it("should mask credentials and query parameters in WebDAV connection strings", () => { + expect( + getRemoteConfigurationDescription( + "sls+webdav://user:pass@example.com/dav?prefix=vault%2F&headers=Authorization%3A%20Bearer%20token" + ) + ).toBe("https://example.com/dav"); + }); +}); diff --git a/src/modules/features/SettingDialogue/remoteConfigBuffer.ts b/src/modules/features/SettingDialogue/remoteConfigBuffer.ts index be23e0d..191a568 100644 --- a/src/modules/features/SettingDialogue/remoteConfigBuffer.ts +++ b/src/modules/features/SettingDialogue/remoteConfigBuffer.ts @@ -1,4 +1,9 @@ -import { pickBucketSyncSettings, pickCouchDBSyncSettings, pickP2PSyncSettings } from "@lib/common/utils.ts"; +import { + pickBucketSyncSettings, + pickCouchDBSyncSettings, + pickP2PSyncSettings, + pickWebDAVSyncSettings, +} from "@lib/common/utils.ts"; import type { ObsidianLiveSyncSettings } from "@lib/common/types.ts"; // Keep the setting dialogue buffer aligned with the current core settings before persisting other dirty keys. @@ -13,5 +18,6 @@ export function syncActivatedRemoteSettings( ...pickBucketSyncSettings(source), ...pickCouchDBSyncSettings(source), ...pickP2PSyncSettings(source), + ...pickWebDAVSyncSettings(source), }); } diff --git a/src/modules/features/SettingDialogue/remoteConfigBuffer.unit.spec.ts b/src/modules/features/SettingDialogue/remoteConfigBuffer.unit.spec.ts index 2e5c4a7..18e1ec1 100644 --- a/src/modules/features/SettingDialogue/remoteConfigBuffer.unit.spec.ts +++ b/src/modules/features/SettingDialogue/remoteConfigBuffer.unit.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO } from "@lib/common/types"; +import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_WEBDAV } from "@lib/common/types"; import { syncActivatedRemoteSettings } from "./remoteConfigBuffer"; describe("syncActivatedRemoteSettings", () => { @@ -80,4 +80,25 @@ describe("syncActivatedRemoteSettings", () => { expect(target.couchDB_PASSWORD).toBe("current-pass"); expect(target.couchDB_DBNAME).toBe("current-db"); }); + + it("should copy active WebDAV connection URI into the editing buffer", () => { + const target = { + ...DEFAULT_SETTINGS, + remoteType: REMOTE_COUCHDB, + activeConfigurationId: "old-remote", + webDAVactiveConnectionURI: "", + }; + const source = { + ...DEFAULT_SETTINGS, + remoteType: REMOTE_WEBDAV, + activeConfigurationId: "remote-webdav", + webDAVactiveConnectionURI: "sls+webdav://user:pass@example.com/dav?prefix=vault%2F", + }; + + syncActivatedRemoteSettings(target, source); + + expect(target.remoteType).toBe(REMOTE_WEBDAV); + expect(target.activeConfigurationId).toBe("remote-webdav"); + expect(target.webDAVactiveConnectionURI).toBe("sls+webdav://user:pass@example.com/dav?prefix=vault%2F"); + }); }); diff --git a/src/modules/features/SettingDialogue/remoteConfigDisplay.ts b/src/modules/features/SettingDialogue/remoteConfigDisplay.ts new file mode 100644 index 0000000..9d96b4c --- /dev/null +++ b/src/modules/features/SettingDialogue/remoteConfigDisplay.ts @@ -0,0 +1,29 @@ +import { ConnectionStringParser } from "@lib/common/ConnectionString.ts"; + +export function getRemoteConfigurationDescription(uri: string): string { + try { + const parsed = ConnectionStringParser.parse(uri); + if (parsed.type === "couchdb") { + const url = new URL(parsed.settings.couchDB_URI); + url.username = ""; + url.password = ""; + url.search = ""; + url.hash = ""; + return url.toString(); + } + if (parsed.type === "s3") { + return `${parsed.settings.endpoint.replace(/\/+$/g, "")}/${parsed.settings.bucket}`; + } + if (parsed.type === "webdav") { + const url = new URL(parsed.settings.webDAVactiveConnectionURI.replace(/^sls\+webdav:/, "https:")); + url.username = ""; + url.password = ""; + url.search = ""; + url.hash = ""; + return url.toString(); + } + return `P2P ${parsed.settings.P2P_roomID || "Remote"}`; + } catch { + return uri.split("@").pop()?.split("?")[0] || ""; + } +} diff --git a/src/modules/features/SetupManager.ts b/src/modules/features/SetupManager.ts index 56287d7..74a9395 100644 --- a/src/modules/features/SetupManager.ts +++ b/src/modules/features/SetupManager.ts @@ -4,12 +4,14 @@ import { type EncryptionSettings, type ObsidianLiveSyncSettings, type P2PSyncSetting, + type WebDAVSyncSetting, DEFAULT_SETTINGS, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, + REMOTE_WEBDAV, } from "@lib/common/types.ts"; import { isObjectDifferent } from "@lib/common/utils.ts"; import Intro from "./SetupWizard/dialogs/Intro.svelte"; @@ -24,6 +26,7 @@ import SetupRemote from "./SetupWizard/dialogs/SetupRemote.svelte"; import SetupRemoteCouchDB from "./SetupWizard/dialogs/SetupRemoteCouchDB.svelte"; import SetupRemoteBucket from "./SetupWizard/dialogs/SetupRemoteBucket.svelte"; import SetupRemoteP2P from "./SetupWizard/dialogs/SetupRemoteP2P.svelte"; +import SetupRemoteWebDAV from "./SetupWizard/dialogs/SetupRemoteWebDAV.svelte"; import SetupRemoteE2EE from "./SetupWizard/dialogs/SetupRemoteE2EE.svelte"; import { decodeSettingsFromQRCodeData } from "@lib/API/processSetting.ts"; import { AbstractModule } from "@/modules/AbstractModule.ts"; @@ -38,6 +41,7 @@ import type { SetupRemoteE2EEResultType, SetupRemoteP2PResultType, SetupRemoteResultType, + SetupRemoteWebDAVResultType, UseSetupURIResultType, } from "./SetupWizard/dialogs/setupDialogTypes.ts"; @@ -202,6 +206,33 @@ export class SetupManager extends AbstractModule { return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate); } + /** + * Handles manual setup for WebDAV-compatible storage. + * @param userMode + * @param currentSetting + * @param activate Whether to activate WebDAV as remote type + * @returns Promise that resolves to true if setup completed successfully, false otherwise + */ + async onWebDAVManualSetup( + userMode: UserMode, + currentSetting: ObsidianLiveSyncSettings, + activate = true + ): Promise { + const webDAVConf = await this.dialogManager.openWithExplicitCancel< + SetupRemoteWebDAVResultType, + WebDAVSyncSetting + >(SetupRemoteWebDAV, currentSetting); + if (webDAVConf === "cancelled") { + this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE); + return await this.onOnboard(userMode); + } + const newSetting = { ...currentSetting, ...webDAVConf } as ObsidianLiveSyncSettings; + if (activate) { + newSetting.remoteType = REMOTE_WEBDAV; + } + return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate); + } + /** * Handles manual setup for P2P * @param userMode @@ -303,6 +334,8 @@ export class SetupManager extends AbstractModule { return await this.onBucketManualSetup(userMode, currentSetting, true); } else if (method === "p2p") { return await this.onP2PManualSetup(userMode, currentSetting, true); + } else if (method === "webdav") { + return await this.onWebDAVManualSetup(userMode, currentSetting, true); } else if (method === "cancelled") { this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE); if (userMode !== UserMode.Unknown) { diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte index 01a299e..b0e8f44 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte @@ -10,6 +10,7 @@ TYPE_COUCHDB, TYPE_BUCKET, TYPE_P2P, + TYPE_WEBDAV, TYPE_CANCELLED, type SetupRemoteResultType, } from "./setupDialogTypes"; @@ -26,12 +27,19 @@ return "Continue to S3/MinIO/R2 setup"; } else if (userType === TYPE_P2P) { return "Continue to Peer-to-Peer only setup"; + } else if (userType === TYPE_WEBDAV) { + return "Continue to WebDAV setup"; } else { return "Please select an option to proceed"; } }); const canProceed = $derived.by(() => { - return userType === TYPE_COUCHDB || userType === TYPE_BUCKET || userType === TYPE_P2P; + return ( + userType === TYPE_COUCHDB || + userType === TYPE_BUCKET || + userType === TYPE_P2P || + userType === TYPE_WEBDAV + ); }); @@ -46,6 +54,10 @@ +