mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-06 16:28:18 +00:00
Merge pull request #1320 from vrtmrz/fix/cli-storage-case-detection
Fix CLI filesystem case-sensitivity detection
This commit is contained in:
6 files changed
+145
-1
No files matched your search
@@ -103,6 +103,44 @@ describe("CLI database preparation", () => {
|
||||
await expect(main(standardIo, runner)).rejects.toThrow(`__EXIT__:${exitCode}`);
|
||||
}
|
||||
|
||||
it.each([
|
||||
{ handleFilenameCaseSensitive: true, pathNormalizationVersion: 0 as const },
|
||||
{ handleFilenameCaseSensitive: true, pathNormalizationVersion: 1 as const },
|
||||
{ handleFilenameCaseSensitive: false, pathNormalizationVersion: 0 as const },
|
||||
{ handleFilenameCaseSensitive: false, pathNormalizationVersion: 1 as const },
|
||||
])(
|
||||
"writes Vault files with case sensitivity $handleFilenameCaseSensitive and path identity $pathNormalizationVersion",
|
||||
async (settings) => {
|
||||
await writeFile(
|
||||
settingsPath,
|
||||
JSON.stringify({ ...createDefaultCliSettings(), ...settings, isConfigured: true })
|
||||
);
|
||||
const writes: boolean[] = [];
|
||||
let failure: unknown;
|
||||
const runner = vi.fn<CliCommandRunner>(async (_options, { core }) => {
|
||||
try {
|
||||
writes.push(
|
||||
await core.serviceModules.storageAccess.writeFileAuto("notes/Remote.md", "remote content")
|
||||
);
|
||||
writes.push(
|
||||
await core.serviceModules.storageAccess.writeFileAuto("notes/Remote.md", "updated content")
|
||||
);
|
||||
} catch (error) {
|
||||
failure = error;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
await start("ls", runner);
|
||||
|
||||
expect(runner).toHaveBeenCalledOnce();
|
||||
expect(failure).toBeUndefined();
|
||||
expect(writes).toEqual([true, true]);
|
||||
expect(await readFile(join(vaultPath, "notes/Remote.md"), "utf8")).toBe("updated content");
|
||||
expect(await readFile(join(vaultPath, "notes/local.md"), "utf8")).toBe("local content");
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ separator: "/", platform: "POSIX" },
|
||||
{ separator: "\\", platform: "Windows" },
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"buildRun": "npm run build && npm run cli --",
|
||||
"build:docker": "docker build -f Dockerfile -t livesync-cli ../../..",
|
||||
"check": "tsc -p tsconfig.json",
|
||||
"test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/main.bootstrap.unit.spec.ts src/apps/cli/settingsPersistence.unit.spec.ts src/apps/cli/runtimeState.unit.spec.ts src/apps/cli/services/NodeKeyValueDBService.unit.spec.ts src/apps/cli/services/NodeLocalStorage.unit.spec.ts src/apps/cli/managers/CLIStorageEventManagerAdapter.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/journalSkip.unit.spec.ts src/apps/cli/commands/daemonCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts src/apps/cli/deploy/install.unit.spec.ts src/apps/cli/adapters/NodeFileSystemAdapter.unit.spec.ts",
|
||||
"test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/main.bootstrap.unit.spec.ts src/apps/cli/settingsPersistence.unit.spec.ts src/apps/cli/runtimeState.unit.spec.ts src/apps/cli/services/NodeKeyValueDBService.unit.spec.ts src/apps/cli/services/NodeLocalStorage.unit.spec.ts src/apps/cli/services/storageCaseSensitivity.unit.spec.ts src/apps/cli/managers/CLIStorageEventManagerAdapter.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/journalSkip.unit.spec.ts src/apps/cli/commands/daemonCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts src/apps/cli/deploy/install.unit.spec.ts src/apps/cli/adapters/NodeFileSystemAdapter.unit.spec.ts",
|
||||
"test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh",
|
||||
"test:e2e:two-vaults:common": "bash test/test-e2e-two-vaults-common.sh",
|
||||
"test:e2e:two-vaults:matrix": "bash test/test-e2e-two-vaults-matrix.sh",
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/
|
||||
import { HeadlessAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/headless/HeadlessAPIService";
|
||||
import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
|
||||
import { createNodeLocalStorage } from "./NodeLocalStorage";
|
||||
import { isStorageCaseInsensitive } from "./storageCaseSensitivity";
|
||||
import { DatabaseService } from "@vrtmrz/livesync-commonlib/compat/services/base/DatabaseService";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { fs, path as nodePath } from "@vrtmrz/livesync-commonlib/node";
|
||||
@@ -135,6 +136,10 @@ export class NodeServiceHub<T extends NodeServiceContext> extends InjectableServ
|
||||
settingService: setting,
|
||||
APIService: API,
|
||||
});
|
||||
let storageInsensitive: boolean | undefined;
|
||||
vault.isStorageInsensitive.setHandler(
|
||||
() => (storageInsensitive ??= isStorageCaseInsensitive(context.runtimeVaultPath))
|
||||
);
|
||||
const scopeDirectory = () => runtimeScopeDirectory(basePath, vault.getVaultName(), context.runtimeVaultPath);
|
||||
const test = new InjectableTestService(context);
|
||||
const databaseEvents = new InjectableDatabaseEventService(context);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { fs, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
|
||||
export function isStorageCaseInsensitive(vaultPath: string): boolean {
|
||||
// Probe with an empty directory on the Vault filesystem. The dot-prefixed name is
|
||||
// excluded by the CLI watcher and synchronisation filters; finally removes it after detection.
|
||||
const probePath = fs.mkdtempSync(path.join(vaultPath, ".livesync-case-"));
|
||||
try {
|
||||
const original = fs.statSync(probePath);
|
||||
const alternate = fs.statSync(path.join(vaultPath, path.basename(probePath).toUpperCase()), {
|
||||
throwIfNoEntry: false,
|
||||
});
|
||||
return alternate !== undefined && original.dev === alternate.dev && original.ino === alternate.ino;
|
||||
} finally {
|
||||
fs.rmdirSync(probePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fs, os, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { isStorageCaseInsensitive } from "./storageCaseSensitivity";
|
||||
import { NodeServiceContext, NodeServiceHub } from "./NodeServiceHub";
|
||||
|
||||
vi.mock("@vrtmrz/livesync-commonlib/node", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@vrtmrz/livesync-commonlib/node")>();
|
||||
return { ...actual, fs: { ...actual.fs } };
|
||||
});
|
||||
|
||||
describe("CLI storage case sensitivity", () => {
|
||||
let directory: string;
|
||||
let vaultPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
directory = fs.mkdtempSync(path.join(os.tmpdir(), "livesync-storage-case-"));
|
||||
vaultPath = path.join(directory, "vault");
|
||||
fs.mkdirSync(vaultPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("matches the Vault filesystem and leaves existing files untouched", () => {
|
||||
fs.writeFileSync(path.join(vaultPath, "Note.md"), "local content");
|
||||
const expected = fs.existsSync(path.join(vaultPath, "NOTE.MD"));
|
||||
|
||||
expect(isStorageCaseInsensitive(vaultPath)).toBe(expected);
|
||||
|
||||
expect(fs.readdirSync(vaultPath)).toEqual(["Note.md"]);
|
||||
expect(fs.readFileSync(path.join(vaultPath, "Note.md"), "utf8")).toBe("local content");
|
||||
});
|
||||
|
||||
it("recognises an alternate spelling of the same filesystem entry", () => {
|
||||
const entry = fs.statSync(vaultPath);
|
||||
vi.spyOn(fs, "statSync").mockReturnValue(entry);
|
||||
|
||||
expect(isStorageCaseInsensitive(vaultPath)).toBe(true);
|
||||
expect(fs.readdirSync(vaultPath)).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not mistake a different entry for the probe", () => {
|
||||
const original = fs.statSync(vaultPath);
|
||||
const other = fs.statSync(directory);
|
||||
vi.spyOn(fs, "statSync").mockReturnValueOnce(original).mockReturnValueOnce(other);
|
||||
|
||||
expect(isStorageCaseInsensitive(vaultPath)).toBe(false);
|
||||
expect(fs.readdirSync(vaultPath)).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports inspection failures and removes its probe", () => {
|
||||
const failure = Object.assign(new Error("Cannot inspect the Vault"), { code: "EACCES" });
|
||||
vi.spyOn(fs, "statSync").mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
|
||||
expect(() => isStorageCaseInsensitive(vaultPath)).toThrow(failure);
|
||||
expect(fs.readdirSync(vaultPath)).toEqual([]);
|
||||
});
|
||||
|
||||
it("lazily detects the selected Vault once per service hub", () => {
|
||||
const createProbe = vi.spyOn(fs, "mkdtempSync");
|
||||
const context = new NodeServiceContext(
|
||||
directory,
|
||||
{
|
||||
readStdin: async () => "",
|
||||
prompt: async () => "",
|
||||
writeStdout: () => undefined,
|
||||
writeStderr: () => undefined,
|
||||
},
|
||||
vaultPath
|
||||
);
|
||||
const hub = new NodeServiceHub(directory, context);
|
||||
expect(createProbe).not.toHaveBeenCalled();
|
||||
|
||||
const first = hub.vault.isStorageInsensitive();
|
||||
expect(hub.vault.isStorageInsensitive()).toBe(first);
|
||||
expect(createProbe).toHaveBeenCalledExactlyOnceWith(path.join(vaultPath, ".livesync-case-"));
|
||||
expect(fs.readdirSync(vaultPath)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -48,6 +48,8 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
||||
|
||||
#### Fixed
|
||||
|
||||
- CLI `fetch` and `daemon` can write received files to the Vault when `handleFilenameCaseSensitive` is enabled. The CLI now detects whether the Vault filesystem distinguishes letter case. (#1319)
|
||||
|
||||
- Commonlib warnings now appear on stderr without `--debug`, so deliberately skipped Journals and other important notices remain visible during ordinary CLI use.
|
||||
|
||||
- NFC file lookups and directory scans report permission and I/O failures instead of treating unreadable files as absent or returning a successful partial scan. A scan retains the comparison settings with which it began.
|
||||
|
||||
Reference in new issue
Block a user