Compare commits

..

1 Commits

Author SHA1 Message Date
vorotamoroz 801bac9e09 fix: handle successful fallback API responses 2026-07-03 08:24:37 +00:00
23 changed files with 31 additions and 474 deletions
+1 -7
View File
@@ -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+webdav://...`, `sls+p2p://...`) to import a remote configuration profile.
- **📥 Import connection**: Paste a connection string (e.g., `sls+https://...`, `sls+s3://...`, `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,12 +164,6 @@ 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
-5
View File
@@ -64,11 +64,6 @@
"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",
+1 -21
View File
@@ -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, REMOTE_WEBDAV } from "@lib/common/types";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P } from "@lib/common/types";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { runCommand } from "./runCommand";
import type { CLIOptions } from "./types";
@@ -205,22 +205,6 @@ 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", () => {
@@ -580,10 +564,6 @@ 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();
+1 -1
Submodule src/lib updated: 2ee5d2055a...5c1033485e
+3 -3
View File
@@ -1,5 +1,5 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { REMOTE_P2P, isJournalRemoteType, type RemoteDBSettings } from "@lib/common/types";
import { REMOTE_MINIO, REMOTE_P2P, 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<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
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 (isJournalRemoteType(settings.remoteType) || settings.remoteType == REMOTE_P2P) {
if (settings.remoteType == REMOTE_MINIO || 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<boolean> {
if (this.services.appLifecycle.isSuspended()) return Promise.resolve(true);
if (!this.services.appLifecycle.isReady()) return Promise.resolve(true);
if (!isJournalRemoteType(this.settings.remoteType) && this.settings.remoteType != REMOTE_P2P) {
if (this.settings.remoteType != REMOTE_MINIO && this.settings.remoteType != REMOTE_P2P) {
const LiveSyncEnabled = this.settings.liveSync;
const continuous = LiveSyncEnabled;
const eventualOnStart = !LiveSyncEnabled && this.settings.syncOnStart;
+2 -2
View File
@@ -1,4 +1,4 @@
import { isJournalRemoteType, type RemoteDBSettings } from "@lib/common/types";
import { REMOTE_MINIO, 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<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
const settings = { ...this.settings, ...settingOverride };
if (isJournalRemoteType(settings.remoteType)) {
if (settings.remoteType == REMOTE_MINIO) {
return Promise.resolve(new LiveSyncJournalReplicator(this.core));
}
return Promise.resolve(false);
@@ -12,7 +12,6 @@ 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";
@@ -65,7 +64,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 { createJournalStorageAdapter } from "@lib/replication/journal/objectstore/JournalStorageAdapterFactory.js";
import { MinioStorageAdapter } from "@lib/replication/journal/objectstore/MinioStorageAdapter.js";
// For creating a document
// const toc = new Set<string>();
@@ -531,10 +530,6 @@ 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),
@@ -542,14 +537,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
onlyOnCouchDBOrMinIO = () =>
({
visibility:
this.isConfiguredAs("remoteType", REMOTE_COUCHDB) ||
isJournalRemoteType(this.editingSettings.remoteType),
this.isConfiguredAs("remoteType", REMOTE_COUCHDB) || this.isConfiguredAs("remoteType", REMOTE_MINIO),
}) as OnUpdateResult;
// E2EE Function
checkWorkingPassphrase = async (): Promise<boolean> => {
if (isJournalRemoteType(this.editingSettings.remoteType)) {
return true;
}
if (this.editingSettings.remoteType == REMOTE_MINIO) return true;
const settingForCheck: RemoteDBSettings = {
...this.editingSettings,
@@ -864,7 +856,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.core.settings,
this.core.simpleStore,
this.core,
createJournalStorageAdapter(this.core.settings, this.core)
new MinioStorageAdapter(this.core.settings, this.core)
);
}
async resetRemoteBucket() {
@@ -163,7 +163,7 @@ export function paneMaintenance(
Logger(`Journal received history has been cleared.`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(this.onlyOnJournalSync);
.addOnUpdate(this.onlyOnMinIO);
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.onlyOnJournalSync);
.addOnUpdate(this.onlyOnMinIO);
});
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.onlyOnJournalSync);
.addOnUpdate(this.onlyOnMinIO);
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.onlyOnJournalSync);
.addOnUpdate(this.onlyOnMinIO);
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.onlyOnJournalSync);
.addOnUpdate(this.onlyOnMinIO);
});
void addPanel(paneEl, "Reset").then((paneEl) => {
@@ -2,7 +2,6 @@ import {
REMOTE_COUCHDB,
REMOTE_MINIO,
REMOTE_P2P,
REMOTE_WEBDAV,
DEFAULT_SETTINGS,
LOG_LEVEL_NOTICE,
type ObsidianLiveSyncSettings,
@@ -34,9 +33,7 @@ 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;
@@ -72,9 +69,6 @@ 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 });
}
@@ -98,14 +92,6 @@ 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"}`;
}
@@ -212,7 +198,7 @@ export function paneRemoteConfig(
};
const runRemoteSetup = async (
baseSettings: ObsidianLiveSyncSettings,
remoteType?: typeof REMOTE_COUCHDB | typeof REMOTE_MINIO | typeof REMOTE_P2P | typeof REMOTE_WEBDAV
remoteType?: typeof REMOTE_COUCHDB | typeof REMOTE_MINIO | typeof REMOTE_P2P
): Promise<ObsidianLiveSyncSettings | false> => {
const setupManager = this.core.getModule(SetupManager);
const dialogManager = setupManager.dialogManager;
@@ -224,13 +210,7 @@ export function paneRemoteConfig(
return false;
}
targetRemoteType =
method === "bucket"
? REMOTE_MINIO
: method === "p2p"
? REMOTE_P2P
: method === "webdav"
? REMOTE_WEBDAV
: REMOTE_COUCHDB;
method === "bucket" ? REMOTE_MINIO : method === "p2p" ? REMOTE_P2P : REMOTE_COUCHDB;
}
if (targetRemoteType === REMOTE_MINIO) {
@@ -249,14 +229,6 @@ 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;
@@ -359,7 +331,7 @@ export function paneRemoteConfig(
for (const config of Object.values(configs)) {
const row = new Setting(listContainer)
.setName(config.name)
.setDesc(getRemoteConfigurationDescription(config.uri));
.setDesc(config.uri.split("@").pop() || ""); // Show host part for privacy
if (config.id === this.editingSettings.activeConfigurationId) {
row.nameEl.addClass("sls-active-remote-name");
@@ -384,8 +356,6 @@ 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;
}
@@ -496,8 +466,6 @@ 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;
}
@@ -1,12 +0,0 @@
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");
});
});
@@ -1,9 +1,4 @@
import {
pickBucketSyncSettings,
pickCouchDBSyncSettings,
pickP2PSyncSettings,
pickWebDAVSyncSettings,
} from "@lib/common/utils.ts";
import { pickBucketSyncSettings, pickCouchDBSyncSettings, pickP2PSyncSettings } 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.
@@ -18,6 +13,5 @@ export function syncActivatedRemoteSettings(
...pickBucketSyncSettings(source),
...pickCouchDBSyncSettings(source),
...pickP2PSyncSettings(source),
...pickWebDAVSyncSettings(source),
});
}
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_WEBDAV } from "@lib/common/types";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO } from "@lib/common/types";
import { syncActivatedRemoteSettings } from "./remoteConfigBuffer";
describe("syncActivatedRemoteSettings", () => {
@@ -80,25 +80,4 @@ 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");
});
});
@@ -1,29 +0,0 @@
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] || "";
}
}
-33
View File
@@ -4,14 +4,12 @@ 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";
@@ -26,7 +24,6 @@ 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";
@@ -41,7 +38,6 @@ import type {
SetupRemoteE2EEResultType,
SetupRemoteP2PResultType,
SetupRemoteResultType,
SetupRemoteWebDAVResultType,
UseSetupURIResultType,
} from "./SetupWizard/dialogs/setupDialogTypes.ts";
@@ -206,33 +202,6 @@ 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<boolean> {
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
@@ -334,8 +303,6 @@ 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) {
@@ -10,7 +10,6 @@
TYPE_COUCHDB,
TYPE_BUCKET,
TYPE_P2P,
TYPE_WEBDAV,
TYPE_CANCELLED,
type SetupRemoteResultType,
} from "./setupDialogTypes";
@@ -27,19 +26,12 @@
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 ||
userType === TYPE_WEBDAV
);
return userType === TYPE_COUCHDB || userType === TYPE_BUCKET || userType === TYPE_P2P;
});
</script>
@@ -54,10 +46,6 @@
<Option selectedValue={TYPE_BUCKET} title="S3/MinIO/R2 Object Storage" bind:value={userType}>
Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.
</Option>
<Option selectedValue={TYPE_WEBDAV} title="WebDAV (Experimental)" bind:value={userType}>
Experimental synchronisation utilising journal files stored in a WebDAV collection. This is a proof of
concept for pluggable Journal storage backends.
</Option>
<Option selectedValue={TYPE_P2P} title="Peer-to-Peer only" bind:value={userType}>
This feature enables direct synchronisation between devices. No server is required, but both devices must be
online at the same time for synchronisation to occur, and some features may be limited. Internet connection
@@ -1,160 +0,0 @@
<script lang="ts">
import DialogHeader from "@lib/UI/components/DialogHeader.svelte";
import Guidance from "@lib/UI/components/Guidance.svelte";
import Decision from "@lib/UI/components/Decision.svelte";
import UserDecisions from "@lib/UI/components/UserDecisions.svelte";
import InfoNote from "@lib/UI/components/InfoNote.svelte";
import InputRow from "@lib/UI/components/InputRow.svelte";
import {
DEFAULT_SETTINGS,
PREFERRED_JOURNAL_SYNC,
RemoteTypes,
type ObsidianLiveSyncSettings,
type WebDAVSyncSetting,
} from "@lib/common/types";
import { copyTo, pickWebDAVSyncSettings } from "@lib/common/utils";
import { getDialogContext, type GuestDialogProps } from "@lib/UI/svelteDialog";
import { onMount } from "svelte";
import { TYPE_CANCELLED, type SetupRemoteWebDAVResultType } from "./setupDialogTypes";
const default_setting = pickWebDAVSyncSettings(DEFAULT_SETTINGS);
let syncSetting = $state<WebDAVSyncSetting>({ ...default_setting });
type Props = GuestDialogProps<SetupRemoteWebDAVResultType, WebDAVSyncSetting>;
const { setResult, getInitialData }: Props = $props();
onMount(() => {
if (getInitialData) {
const initialData = getInitialData();
if (initialData) {
copyTo(initialData, syncSetting);
}
}
});
let error = $state("");
let processing = $state(false);
const context = getDialogContext();
function parseURI(): URL | false {
const match = syncSetting.webDAVactiveConnectionURI.trim().match(/^sls\+webdav:(.*)$/);
if (!match) {
return false;
}
try {
return new URL(`https:${match[1]}`);
} catch {
return false;
}
}
const parsedURI = $derived.by(() => parseURI());
const canProceed = $derived.by(() => parsedURI !== false);
const isInsecure = $derived.by(() => parsedURI !== false && parsedURI.searchParams.get("insecure") === "true");
const usesInternalAPI = $derived.by(
() => parsedURI !== false && parsedURI.searchParams.get("useProxy") === "true"
);
function generateSetting() {
const trialSettings: WebDAVSyncSetting = {
...syncSetting,
webDAVactiveConnectionURI: syncSetting.webDAVactiveConnectionURI.trim(),
};
const trialRemoteSetting: ObsidianLiveSyncSettings = {
...DEFAULT_SETTINGS,
...PREFERRED_JOURNAL_SYNC,
remoteType: RemoteTypes.REMOTE_WEBDAV,
...trialSettings,
};
return trialRemoteSetting;
}
async function checkConnection() {
try {
processing = true;
const trialRemoteSetting = generateSetting();
const replicator = await context.services.replicator.getNewReplicator(trialRemoteSetting);
if (!replicator) {
return "Failed to create replicator instance.";
}
try {
const result = await replicator.tryConnectRemote(trialRemoteSetting, false);
if (result) {
return "";
}
return "Failed to connect to the server. Please check your settings.";
} catch (e) {
return `Failed to connect to the server: ${e}`;
}
} finally {
processing = false;
}
}
async function checkAndCommit() {
error = "";
try {
error = (await checkConnection()) || "";
if (!error) {
setResult(pickWebDAVSyncSettings(generateSetting()));
return;
}
} catch (e) {
error = `Error during connection test: ${e}`;
}
}
function commit() {
setResult(pickWebDAVSyncSettings(generateSetting()));
}
function cancel() {
setResult(TYPE_CANCELLED);
}
</script>
<DialogHeader title="WebDAV Configuration" />
<Guidance>
Please enter the WebDAV connection URI. This experimental setup stores Journal synchronisation files in a WebDAV
collection.
</Guidance>
<InputRow label="Connection URI">
<input
type="text"
name="webdav-connection-uri"
placeholder="sls+webdav://user:password@example.com/dav?prefix=vault%2F"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
required
bind:value={syncSetting.webDAVactiveConnectionURI}
/>
</InputRow>
<InfoNote>
Use `sls+webdav://user:password@host/path`. Optional query parameters include `prefix`, `useProxy=true`, and
`insecure=true` for local HTTP testing.
</InfoNote>
<InfoNote warning visible={isInsecure}>Secure HTTPS connections are required on Obsidian Mobile.</InfoNote>
<InfoNote visible={usesInternalAPI}>
This connection uses Obsidian's internal API. It can help when direct WebDAV requests are blocked by CORS.
</InfoNote>
<InfoNote error visible={syncSetting.webDAVactiveConnectionURI.trim() !== "" && !canProceed}>
The connection URI must start with `sls+webdav://`.
</InfoNote>
<InfoNote error visible={error !== ""}>
{error}
</InfoNote>
{#if processing}
Checking connection... Please wait.
{:else}
<UserDecisions>
<Decision title="Test Settings and Continue" important disabled={!canProceed} commit={() => checkAndCommit()} />
<Decision title="Continue anyway" disabled={!canProceed} commit={() => commit()} />
<Decision title="Cancel" commit={() => cancel()} />
</UserDecisions>
{/if}
@@ -4,7 +4,6 @@ import type {
EncryptionSettings,
ObsidianLiveSyncSettings,
P2PConnectionInfo,
WebDAVSyncSetting,
} from "@lib/common/models/setting.type";
export const TYPE_IDENTICAL = "identical";
@@ -41,7 +40,6 @@ export const TYPE_CLOSE = "close";
export const TYPE_COUCHDB = "couchdb";
export const TYPE_BUCKET = "bucket";
export const TYPE_P2P = "p2p";
export const TYPE_WEBDAV = "webdav";
export type ResultTypeVault =
| typeof TYPE_IDENTICAL
@@ -95,12 +93,7 @@ export type SelectMethodExistingResultType =
| typeof TYPE_CONFIGURE_MANUALLY
| typeof TYPE_CANCELLED;
export type SetupRemoteResultType =
| typeof TYPE_COUCHDB
| typeof TYPE_BUCKET
| typeof TYPE_P2P
| typeof TYPE_WEBDAV
| typeof TYPE_CANCELLED;
export type SetupRemoteResultType = typeof TYPE_COUCHDB | typeof TYPE_BUCKET | typeof TYPE_P2P | typeof TYPE_CANCELLED;
export type UseSetupURIResultType = typeof TYPE_CANCELLED | ObsidianLiveSyncSettings;
@@ -112,6 +105,4 @@ export type SetupRemoteCouchDBResultType = typeof TYPE_CANCELLED | CouchDBConnec
export type SetupRemoteP2PResultType = typeof TYPE_CANCELLED | P2PConnectionInfo;
export type SetupRemoteWebDAVResultType = typeof TYPE_CANCELLED | WebDAVSyncSetting;
export type ScanQRCodeResultType = typeof TYPE_CLOSE;
+7 -5
View File
@@ -147,12 +147,14 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
: req instanceof Request && typeof req.method === "string"
? req.method
: "GET";
if (opts?.body) {
body = typeof opts.body === "string" ? opts.body : await new Response(opts.body).arrayBuffer();
} else if (typeof req !== "string" && req.body) {
body = await new Response(req.body).arrayBuffer();
if (typeof req !== "string") {
if (opts?.body) {
body = typeof opts.body === "string" ? opts.body : await new Response(opts.body).arrayBuffer();
} else if (req.body) {
body = await new Response(req.body).arrayBuffer();
}
} else {
body = undefined;
body = opts?.body as string;
}
const reqHeaders = new Headers(req instanceof Request ? req.headers : {});
@@ -1,63 +0,0 @@
import { describe, expect, it, vi } from "vitest";
const requestUrlMock = vi.hoisted(() => vi.fn());
vi.mock("@/deps", () => {
return {
requestUrl: requestUrlMock,
Notice: vi.fn(),
Platform: {},
};
});
vi.mock("@/deps.ts", () => {
return {
requestUrl: requestUrlMock,
Notice: vi.fn(),
Platform: {},
};
});
vi.mock("./ObsidianConfirm", () => ({
ObsidianConfirm: class {},
}));
import { ObsidianAPIService } from "./ObsidianAPIService.ts";
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts";
describe("ObsidianAPIService.nativeFetch", () => {
it("should pass binary string-request bodies to requestUrl as ArrayBuffer", async () => {
requestUrlMock.mockResolvedValueOnce({
arrayBuffer: new TextEncoder().encode("ok").buffer,
headers: {},
status: 207,
});
const service = new ObsidianAPIService({} as ObsidianServiceContext);
const body = new TextEncoder().encode("payload");
const response = await service.nativeFetch("https://webdav.example.com/file", {
method: "PUT",
body: body as unknown as BodyInit,
headers: {
Depth: "1",
Authorization: "Basic dXNlcjpwYXNz",
},
});
expect(response.status).toBe(207);
expect(requestUrlMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://webdav.example.com/file",
method: "PUT",
body: expect.any(ArrayBuffer),
headers: expect.objectContaining({
Depth: "1",
Authorization: "Basic dXNlcjpwYXNz",
}),
throw: false,
})
);
const requestBody = requestUrlMock.mock.calls[0][0].body as ArrayBuffer;
expect(new TextDecoder().decode(requestBody)).toBe("payload");
});
});
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
set -e
docker inspect webdav-test >/dev/null
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
set -e
webdavUsername=${webdavUsername:-webdav}
webdavPassword=${webdavPassword:-webdav}
webdavPort=${webdavPort:-8088}
docker run -d \
--name webdav-test \
-p "$webdavPort:8080" \
rclone/rclone serve webdav /data \
--addr :8080 \
--baseurl /dav \
--user "$webdavUsername" \
--pass "$webdavPassword"
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
docker stop webdav-test
docker rm webdav-test
-7
View File
@@ -3,13 +3,6 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## Unreleased
### Experimental
- Added experimental WebDAV support for Journal synchronisation storage, including setup dialogue integration. This is currently a proof of concept for injecting Journal storage backends through the replication layer, and should be treated as an implementation experiment rather than a fully supported setup flow.
- Hardened the WebDAV Journal storage proof of concept by centralising Journal storage adapter selection, improving connection checks, masking remote configuration descriptions, and verifying the Docker WebDAV integration path.
## 0.25.79
29th June, 2026