mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-07 13:25:28 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f9446f447 | |||
| 3f9cd67b1c | |||
| dbcbf2c5ca | |||
| eed0fca8d3 | |||
| 05e031b90b | |||
| bec767c13f | |||
| 8046a777af | |||
| 0c58b0c513 | |||
| b66e227a02 | |||
| af6df84b5d |
@@ -148,6 +148,20 @@ Hence, the new feature should be implemented as follows:
|
||||
- **Service Hub** (`src/modules/services/`): Central service registry using dependency injection
|
||||
- **Common Library** (`src/lib/`): Platform-independent sync logic, shared with other tools
|
||||
|
||||
### Conflict Merge Policy
|
||||
|
||||
Markdown conflict auto-merge should behave like a conservative three-way merge. The guiding rule is to merge changes when they touch non-overlapping regions, and to keep a manual conflict when the edits overlap semantically.
|
||||
|
||||
When in doubt, prefer the safer outcome: preserve data, keep the conflict visible, and ask the user rather than silently discarding content or choosing one side.
|
||||
|
||||
- If one side deletes a line and the other side leaves that same line unchanged, treat it as a safe deletion. The deleted line must not be reintroduced into the merged result.
|
||||
- If one side inserts new content in a different region while the other side deletes an unchanged old region, preserve the insertion and the deletion.
|
||||
- If one side deletes a line and the other side modifies that same line, keep the conflict for user resolution.
|
||||
- If both sides insert different content at the same position, keep both insertions in a deterministic order unless the surrounding deletion context indicates that they are competing replacements.
|
||||
- Avoid resolving conflicts by simply choosing the newest revision unless the user has explicitly selected that behaviour.
|
||||
|
||||
This policy is intentionally aligned with the conflict checkboxes and compatibility settings: automatic merge should remove avoidable prompts, but it must not silently choose between overlapping user intentions.
|
||||
|
||||
### File Structure Conventions
|
||||
|
||||
- **Platform-specific code**: Use `.platform.ts` suffix (replaced with `.obsidian.ts` in production builds via esbuild)
|
||||
|
||||
+1
-7
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ import { EVENT_SETTING_SAVED, eventHub } from "@/common/events.ts";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { tryGetFilePath } from "@lib/common/utils.doc.ts";
|
||||
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
|
||||
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
|
||||
|
||||
declare global {
|
||||
@@ -1832,43 +1833,35 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
||||
}
|
||||
|
||||
async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
||||
if (
|
||||
mode != "FETCH" &&
|
||||
mode != "OVERWRITE" &&
|
||||
mode != "MERGE" &&
|
||||
mode != "DISABLE" &&
|
||||
mode != "DISABLE_HIDDEN"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == "DISABLE" || mode == "DISABLE_HIDDEN") {
|
||||
// await this.core.$allSuspendExtraSync();
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
syncInternalFiles: false,
|
||||
},
|
||||
true
|
||||
);
|
||||
// this.core.settings.syncInternalFiles = false;
|
||||
// await this.core.saveSettings();
|
||||
return;
|
||||
}
|
||||
this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
|
||||
if (mode == "FETCH") {
|
||||
await this.initialiseInternalFileSync("pullForce", true);
|
||||
} else if (mode == "OVERWRITE") {
|
||||
await this.initialiseInternalFileSync("pushForce", true);
|
||||
} else if (mode == "MERGE") {
|
||||
await this.initialiseInternalFileSync("safe", true);
|
||||
}
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
useAdvancedMode: true,
|
||||
syncInternalFiles: true,
|
||||
const result = await configureHiddenFileSyncMode(mode, {
|
||||
disable: async () => {
|
||||
// await this.core.$allSuspendExtraSync();
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
syncInternalFiles: false,
|
||||
},
|
||||
true
|
||||
);
|
||||
// this.core.settings.syncInternalFiles = false;
|
||||
// await this.core.saveSettings();
|
||||
},
|
||||
true
|
||||
);
|
||||
enable: async () => {
|
||||
this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
useAdvancedMode: true,
|
||||
syncInternalFiles: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
initialise: async (direction) => {
|
||||
await this.initialiseInternalFileSync(direction, true);
|
||||
},
|
||||
});
|
||||
if (result == "ignored" || result == "disabled") {
|
||||
return;
|
||||
}
|
||||
// this.plugin.settings.useAdvancedMode = true;
|
||||
// this.plugin.settings.syncInternalFiles = true;
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
type HiddenFileSyncMode = "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN";
|
||||
type HiddenFileSyncDirection = "pullForce" | "pushForce" | "safe";
|
||||
|
||||
type ConfigureHiddenFileSyncHandlers = {
|
||||
disable: () => Promise<void>;
|
||||
enable: () => Promise<void>;
|
||||
initialise: (direction: HiddenFileSyncDirection) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ConfigureHiddenFileSyncResult = "ignored" | "disabled" | "enabled";
|
||||
|
||||
function getInitialiseDirection(mode: keyof OPTIONAL_SYNC_FEATURES): HiddenFileSyncDirection | false {
|
||||
if (mode == "FETCH") return "pullForce";
|
||||
if (mode == "OVERWRITE") return "pushForce";
|
||||
if (mode == "MERGE") return "safe";
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDisableMode(mode: keyof OPTIONAL_SYNC_FEATURES): boolean {
|
||||
return mode == "DISABLE" || mode == "DISABLE_HIDDEN";
|
||||
}
|
||||
|
||||
function isHiddenFileSyncMode(mode: keyof OPTIONAL_SYNC_FEATURES): mode is HiddenFileSyncMode {
|
||||
return mode == "FETCH" || mode == "OVERWRITE" || mode == "MERGE" || isDisableMode(mode);
|
||||
}
|
||||
|
||||
export async function configureHiddenFileSyncMode(
|
||||
mode: keyof OPTIONAL_SYNC_FEATURES,
|
||||
handlers: ConfigureHiddenFileSyncHandlers
|
||||
): Promise<ConfigureHiddenFileSyncResult> {
|
||||
if (!isHiddenFileSyncMode(mode)) {
|
||||
return "ignored";
|
||||
}
|
||||
if (isDisableMode(mode)) {
|
||||
await handlers.disable();
|
||||
return "disabled";
|
||||
}
|
||||
const direction = getInitialiseDirection(mode);
|
||||
if (direction === false) {
|
||||
return "ignored";
|
||||
}
|
||||
await handlers.enable();
|
||||
await handlers.initialise(direction);
|
||||
return "enabled";
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
|
||||
|
||||
describe("configureHiddenFileSyncMode", () => {
|
||||
it.each([
|
||||
["FETCH", "pullForce"],
|
||||
["OVERWRITE", "pushForce"],
|
||||
["MERGE", "safe"],
|
||||
] as const)("enables hidden file sync before initialising %s", async (mode, direction) => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const result = await configureHiddenFileSyncMode(mode, {
|
||||
disable: vi.fn(async () => {
|
||||
calls.push("disable");
|
||||
}),
|
||||
enable: vi.fn(async () => {
|
||||
calls.push("enable");
|
||||
}),
|
||||
initialise: vi.fn(async (actualDirection) => {
|
||||
calls.push(`init:${actualDirection}`);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBe("enabled");
|
||||
expect(calls).toEqual(["enable", `init:${direction}`]);
|
||||
});
|
||||
|
||||
it.each(["DISABLE", "DISABLE_HIDDEN"] as const)("disables hidden file sync immediately for %s", async (mode) => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const result = await configureHiddenFileSyncMode(mode, {
|
||||
disable: vi.fn(async () => {
|
||||
calls.push("disable");
|
||||
}),
|
||||
enable: vi.fn(async () => {
|
||||
calls.push("enable");
|
||||
}),
|
||||
initialise: vi.fn(async (direction) => {
|
||||
calls.push(`init:${direction}`);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBe("disabled");
|
||||
expect(calls).toEqual(["disable"]);
|
||||
});
|
||||
});
|
||||
+1
-1
Submodule src/lib updated: 2ee5d2055a...a0efb7274e
@@ -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;
|
||||
|
||||
@@ -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] || "";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
docker inspect webdav-test >/dev/null
|
||||
@@ -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"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
docker stop webdav-test
|
||||
docker rm webdav-test
|
||||
+10
-3
@@ -5,10 +5,17 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Experimental
|
||||
### Fixed
|
||||
|
||||
- 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.
|
||||
- Improved Markdown conflict auto-merge so that non-overlapping edits are merged while overlapping delete-and-edit cases remain visible for manual resolution (#993).
|
||||
- Behaviour change:
|
||||
- When one side deletes an unchanged line and the other side edits a different region, the deleted line is no longer reintroduced into the merged result.
|
||||
- When one side deletes a line and the other side modifies that same line, the conflict is preserved instead of silently choosing one side.
|
||||
- Fixed an issue where applying a newer database entry to storage could incorrectly preserve an older local file as a conflict (#994).
|
||||
- Behaviour change:
|
||||
- Local storage is preserved as a conflict when it may contain unsynchronised changes that are not represented in the revision history. A newer incoming text entry is applied without creating a conflict only when it clearly extends the existing local text.
|
||||
- Fixed an issue where choosing Disable and then Overwrite in Hidden File Sync could silently skip hidden files, because the overwrite setup ran while hidden file synchronisation was still disabled (#989, PR #992).
|
||||
- Hidden File Sync is now re-enabled before the Fetch, Overwrite, or Merge initialisation runs, instead of after it completes. If that initialisation fails, the setting may remain enabled.
|
||||
|
||||
## 0.25.79
|
||||
|
||||
|
||||
Reference in New Issue
Block a user