mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-23 01:23:22 +00:00
Add opt-in Unicode NFC path identity for CouchDB
This commit is contained in:
1 parent
72dc88c7ef
commit
ddb4d52931
39 files changed
+2245
-97
No files matched your search
@@ -6,6 +6,7 @@ import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
type EntryDoc,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { isRemediationModeActive } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
||||
import {
|
||||
activateRemoteConfiguration,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
} from "@vrtmrz/livesync-commonlib/remote-configurations";
|
||||
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
|
||||
import type { CLICommandContext, CLIOptions } from "./types";
|
||||
import { toArrayBuffer, toDatabaseRelativePath } from "./utils";
|
||||
import { getForceRequirementError, toArrayBuffer, toDatabaseRelativePath } from "./utils";
|
||||
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { fsPromises as fs, getNodeFileCreationTime, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
@@ -37,8 +38,25 @@ function redactConnectionString(uri: string): string {
|
||||
}
|
||||
|
||||
export async function runCommand(options: CLIOptions, context: CLICommandContext): Promise<boolean> {
|
||||
const { databasePath, core, replicationScheduling, settingsPath } = context;
|
||||
const { core } = context;
|
||||
const { standardIo } = core.services.context;
|
||||
const forceRequirementError = getForceRequirementError(options.command, options.force);
|
||||
if (forceRequirementError) {
|
||||
writeStderrLine(standardIo, `[Error] ${forceRequirementError}`);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
options.command === "fetch" &&
|
||||
isRemediationModeActive(core.services.setting.currentSettings())
|
||||
) {
|
||||
writeStderrLine(
|
||||
standardIo,
|
||||
`[Error] The fetch command cannot run while remediation mode is active. Clear the modification-time limit before fetching.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { databasePath, replicationScheduling, settingsPath } = context;
|
||||
const vaultPath = context.vaultPath || databasePath;
|
||||
|
||||
await core.services.control.activated;
|
||||
@@ -519,6 +537,29 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.command === "rebuild") {
|
||||
writeStderrLine(standardIo, "[Command] rebuild");
|
||||
try {
|
||||
await core.serviceModules.rebuilder.$rebuildEverything();
|
||||
return true;
|
||||
} catch (error) {
|
||||
writeStderrLine(standardIo, "[Error] Rebuild failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.command === "fetch") {
|
||||
writeStderrLine(standardIo, "[Command] fetch");
|
||||
try {
|
||||
// Fetch replaces the local database from remote; do not persist legacy local files first.
|
||||
await core.serviceModules.rebuilder.$fetchLocal(false, true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
writeStderrLine(standardIo, "[Error] Fetch failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.command === "remote-add") {
|
||||
if (options.commandArgs.length < 2) {
|
||||
throw new Error("remote-add requires two arguments: <name> <connstr>");
|
||||
|
||||
@@ -118,6 +118,10 @@ function createCoreMock() {
|
||||
fetch: vi.fn(async () => undefined),
|
||||
storeContent: vi.fn(async () => true),
|
||||
},
|
||||
rebuilder: {
|
||||
$rebuildEverything: vi.fn(async () => undefined),
|
||||
$fetchLocal: vi.fn(async () => undefined),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
@@ -268,6 +272,73 @@ describe("runCommand abnormal cases", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each(["rebuild", "fetch"] as const)("requires --force before activating for %s", async (command) => {
|
||||
const core = createCoreMock();
|
||||
const activated = vi.fn(() => Promise.resolve());
|
||||
Object.defineProperty(core.services.control, "activated", { get: activated });
|
||||
|
||||
await expect(runCommand(makeOptions(command, []), { ...context, core })).resolves.toBe(false);
|
||||
|
||||
expect(activated).not.toHaveBeenCalled();
|
||||
expect(core.serviceModules.rebuilder.$rebuildEverything).not.toHaveBeenCalled();
|
||||
expect(core.serviceModules.rebuilder.$fetchLocal).not.toHaveBeenCalled();
|
||||
expect(core.services.context.standardIo.writeStderr).toHaveBeenCalledWith(
|
||||
expect.stringContaining("requires --force")
|
||||
);
|
||||
});
|
||||
|
||||
it("dispatches a forced rebuild to the existing Rebuilder service", async () => {
|
||||
const core = createCoreMock();
|
||||
const options = makeOptions("rebuild", []);
|
||||
options.force = true;
|
||||
|
||||
await expect(runCommand(options, { ...context, core })).resolves.toBe(true);
|
||||
|
||||
expect(core.serviceModules.rebuilder.$rebuildEverything).toHaveBeenCalledOnce();
|
||||
expect(core.serviceModules.rebuilder.$fetchLocal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("dispatches a forced fetch without saving legacy Vault files first", async () => {
|
||||
const core = createCoreMock();
|
||||
const options = makeOptions("fetch", []);
|
||||
options.force = true;
|
||||
|
||||
await expect(runCommand(options, { ...context, core })).resolves.toBe(true);
|
||||
|
||||
expect(core.serviceModules.rebuilder.$fetchLocal).toHaveBeenCalledOnce();
|
||||
expect(core.serviceModules.rebuilder.$fetchLocal).toHaveBeenCalledWith(false, true);
|
||||
expect(core.serviceModules.rebuilder.$rebuildEverything).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects fetch in remediation mode even when forced", async () => {
|
||||
const core = createCoreMock();
|
||||
core.services.setting.currentSettings().maxMTimeForReflectEvents = 1;
|
||||
const options = makeOptions("fetch", []);
|
||||
options.force = true;
|
||||
|
||||
await expect(runCommand(options, { ...context, core })).resolves.toBe(false);
|
||||
|
||||
expect(core.serviceModules.rebuilder.$fetchLocal).not.toHaveBeenCalled();
|
||||
expect(core.services.context.standardIo.writeStderr).toHaveBeenCalledWith(
|
||||
expect.stringContaining("remediation mode")
|
||||
);
|
||||
});
|
||||
|
||||
it("surfaces an internal rebuild refusal as a failed command", async () => {
|
||||
const core = createCoreMock();
|
||||
core.serviceModules.rebuilder.$rebuildEverything.mockRejectedValue(
|
||||
new Error("Rebuilding is not available while remediation mode is active.")
|
||||
);
|
||||
const options = makeOptions("rebuild", []);
|
||||
options.force = true;
|
||||
|
||||
await expect(runCommand(options, { ...context, core })).resolves.toBe(false);
|
||||
|
||||
expect(core.services.context.standardIo.writeStderr).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Rebuilding is not available while remediation mode is active")
|
||||
);
|
||||
});
|
||||
|
||||
it("retains visible progress for the interactive sync command", async () => {
|
||||
const core = createCoreMock();
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ export type CLICommand =
|
||||
| "rm"
|
||||
| "resolve"
|
||||
| "mirror"
|
||||
| "rebuild"
|
||||
| "fetch"
|
||||
| "remote-add"
|
||||
| "remote-rm"
|
||||
| "remote-ls"
|
||||
@@ -100,6 +102,8 @@ export const VALID_COMMANDS = new Set([
|
||||
"rm",
|
||||
"resolve",
|
||||
"mirror",
|
||||
"rebuild",
|
||||
"fetch",
|
||||
"remote-add",
|
||||
"remote-rm",
|
||||
"remote-ls",
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { CLICommand } from "./types";
|
||||
|
||||
export function getForceRequirementError(command: CLICommand, force = false): string | undefined {
|
||||
if (force) return undefined;
|
||||
if (command === "rebuild") {
|
||||
return "Command 'rebuild' requires --force because it replaces the local and remote databases.";
|
||||
}
|
||||
if (command === "fetch") {
|
||||
return "Command 'fetch' requires --force because it replaces the local database with remote state.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function toArrayBuffer(data: Buffer): ArrayBuffer {
|
||||
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import { path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { toDatabaseRelativePath } from "./utils";
|
||||
import { getForceRequirementError, toDatabaseRelativePath } from "./utils";
|
||||
|
||||
describe("getForceRequirementError", () => {
|
||||
it.each([
|
||||
["rebuild", "Command 'rebuild' requires --force because it replaces the local and remote databases."],
|
||||
["fetch", "Command 'fetch' requires --force because it replaces the local database with remote state."],
|
||||
] as const)("requires force for %s", (command, expected) => {
|
||||
expect(getForceRequirementError(command, false)).toBe(expected);
|
||||
expect(getForceRequirementError(command, true)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not require force for ordinary commands", () => {
|
||||
expect(getForceRequirementError("sync", false)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("toDatabaseRelativePath", () => {
|
||||
const databasePath = path.resolve("/tmp/livesync-vault");
|
||||
|
||||
Reference in new issue
Block a user