fix(cli): skip hidden directories during vault scans

This commit is contained in:
vorotamoroz committed 2026-11-08 04:32:14 +00:00
1 parent 003173ee0e
commit 7382e155ba
5 files changed
+58 -2

No files matched your search

@@ -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<NodeFile, NodeF
}
private async scanDirectoryInto(relativePath: string, files: Map<string, NodeFile>): Promise<void> {
if (isHiddenPath(relativePath)) return;
const resolver = this.createScanResolver();
const filePath = parseFilePath(relativePath);
if (resolver) {
@@ -232,6 +234,7 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeF
if (entries === null) return;
for (const entry of entries) {
if (entry.kind === "directory") {
if (isHiddenPath(entry.path)) continue;
await this.scanResolvedDirectoryInto(resolver, entry.path, files);
continue;
}
@@ -261,6 +264,7 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeF
files.set(filePath, file);
}
for (const entryPath of entries.folders) {
if (isHiddenPath(entryPath)) continue;
await this.scanLegacyDirectoryInto(entryPath, files);
}
} catch (error) {
@@ -29,6 +29,53 @@ describe("NodeFileSystemAdapter file enumeration", () => {
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();
+3
View File
@@ -0,0 +1,3 @@
export function isHiddenPath(relativePath: string): boolean {
return relativePath.split("/").some((part) => part.startsWith("."));
}
+2 -2
View File
@@ -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.
+2
View File
@@ -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.