From 7382e155ba93d150dd2ecc2d9be3043cb40c8261 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 8 Nov 2026 04:32:14 +0000 Subject: [PATCH] fix(cli): skip hidden directories during vault scans --- .../cli/adapters/NodeFileSystemAdapter.ts | 4 ++ .../NodeFileSystemAdapter.unit.spec.ts | 47 +++++++++++++++++++ src/apps/cli/cliPathFilter.ts | 3 ++ src/apps/cli/main.ts | 4 +- updates.md | 2 + 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/apps/cli/cliPathFilter.ts diff --git a/src/apps/cli/adapters/NodeFileSystemAdapter.ts b/src/apps/cli/adapters/NodeFileSystemAdapter.ts index 15b50f07..ca7a2ce1 100644 --- a/src/apps/cli/adapters/NodeFileSystemAdapter.ts +++ b/src/apps/cli/adapters/NodeFileSystemAdapter.ts @@ -9,6 +9,7 @@ import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes"; import { path } from "@vrtmrz/livesync-commonlib/node"; import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput"; import { findCaseInsensitiveFile } from "@/apps/caseInsensitiveFileLookup"; +import { isHiddenPath } from "@/apps/cli/cliPathFilter"; import { createStoragePathResolver, parseFilePath, @@ -194,6 +195,7 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter): Promise { + if (isHiddenPath(relativePath)) return; const resolver = this.createScanResolver(); const filePath = parseFilePath(relativePath); if (resolver) { @@ -232,6 +234,7 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter { await Promise.all(tempDirs.splice(0).map((directory) => fs.rm(directory, { recursive: true, force: true }))); }); + it.each([0, 1])("does not read hidden directories in identity mode %s", async (version) => { + const { directory } = await createVault(); + for (const hiddenFile of [".tool/state/private.txt", "folder/.private/secret.md"]) { + await fs.mkdir(path.dirname(path.join(directory, hiddenFile)), { recursive: true }); + await fs.writeFile(path.join(directory, hiddenFile), "excluded hidden content"); + } + const report = vi.fn(); + const adapter = new NodeFileSystemAdapter(directory, report, () => version); + const hiddenReads: string[] = []; + const error = Object.assign(new Error("Hidden directory is unreadable"), { code: "EACCES" }); + const checkDirectory = (relativePath: string) => { + if (relativePath === ".tool" || relativePath === "folder/.private") { + hiddenReads.push(relativePath); + throw error; + } + }; + const readDirectory = adapter.storage.readDirectory.bind(adapter.storage); + vi.spyOn(adapter.storage, "readDirectory").mockImplementation(async (relativePath) => { + checkDirectory(relativePath); + return await readDirectory(relativePath); + }); + const list = adapter.storage.list.bind(adapter.storage); + vi.spyOn(adapter.storage, "list").mockImplementation(async (...args) => { + checkDirectory(args[0]); + return await list(...args); + }); + + expect((await adapter.getFiles()).map((file) => file.path).sort()).toEqual(paths); + await adapter.scanDirectory(); + await adapter.scanDirectory(".tool/state"); + expect(hiddenReads).toEqual([]); + expect(report).not.toHaveBeenCalled(); + }); + + it.each([0, 1])("keeps explicit hidden file lookups available in identity mode %s", async (version) => { + const { directory } = await createVault(); + const hiddenPath = ".tool/state/private.txt"; + await fs.mkdir(path.dirname(path.join(directory, hiddenPath)), { recursive: true }); + await fs.writeFile(path.join(directory, hiddenPath), "explicit hidden content"); + const adapter = new NodeFileSystemAdapter(directory, undefined, () => version); + + expect(await adapter.getAbstractFileByPath(hiddenPath)).toMatchObject({ path: hiddenPath }); + expect(await adapter.storage.read(hiddenPath)).toBe( + "explicit hidden content" + ); + }); + it("lists every file when one file was refreshed before the first enumeration", async () => { const { adapter } = await createVault(); diff --git a/src/apps/cli/cliPathFilter.ts b/src/apps/cli/cliPathFilter.ts new file mode 100644 index 00000000..41447bc5 --- /dev/null +++ b/src/apps/cli/cliPathFilter.ts @@ -0,0 +1,3 @@ +export function isHiddenPath(relativePath: string): boolean { + return relativePath.split("/").some((part) => part.startsWith(".")); +} diff --git a/src/apps/cli/main.ts b/src/apps/cli/main.ts index c40de3e8..35c5038a 100644 --- a/src/apps/cli/main.ts +++ b/src/apps/cli/main.ts @@ -24,6 +24,7 @@ import { getForceRequirementError } from "./commands/utils"; import { getPathFromUXFileInfo } from "@vrtmrz/livesync-commonlib/compat/common/typeUtils"; import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; import { IgnoreRules } from "./serviceModules/IgnoreRules"; +import { isHiddenPath } from "./cliPathFilter"; import { useP2PReplicatorFeature, type UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/p2p"; import { useOfflineScanner } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner"; import type { ReplicationSchedulingControl } from "@/serviceFeatures/replicationScheduling"; @@ -583,8 +584,7 @@ export async function main( core.services.vault.isTargetFile.addHandler(async (target) => { const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target)); const parts = targetPath.split("/"); - // if some part of the path starts with dot, treat it as internal file and ignore. - if (parts.some((part) => part.startsWith("."))) { + if (isHiddenPath(targetPath)) { return await Promise.resolve(false); } // PouchDB LevelDB database directory lives in the vault directory. diff --git a/updates.md b/updates.md index f643dd27..efd72f4f 100644 --- a/updates.md +++ b/updates.md @@ -16,6 +16,8 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi #### Fixed +- The CLI daemon now starts even when hidden directories in our Vault are unreadable. Vault scans skip these directories, which remain excluded from synchronisation. (#1325) + - The daemon now detects files written into newly created folders during a burst of writes, including files renamed from temporary names, without requiring a restart. (#1328) - The CLI requires Node.js 22.22 or later. The Docker image includes its own runtime.