Fix CLI filesystem case-sensitivity detection

This commit is contained in:
vorotamoroz committed 2026-11-05 10:35:41 +00:00
1 parent 525062f1b5
commit daa67bea51
6 files changed
+143 -1

No files matched your search

+5
View File
@@ -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,14 @@
import { fs, path } from "@vrtmrz/livesync-commonlib/node";
export function isStorageCaseInsensitive(vaultPath: string): boolean {
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([]);
});
});