mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-15 04:30:12 +00:00
fix(cli): share Vault-relative path exclusions
Apply watcher and logical-target exclusions within the selected Vault so hidden or database-like parent directories do not suppress monitoring. Protect primary, previous, and view databases, and provide a runtime opt-in for ordinary database-like directories without changing database ownership. Cover hidden roots, symlink restarts, exclusion parity, and encrypted CouchDB create, modify, and delete operations with regression tests. Refs #1340
This commit is contained in:
1 parent
78ebc2d298
commit
116c2dfbfb
20 files changed
+1703
-107
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, symlink, unlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -66,6 +66,118 @@ for (let directoryIndex = 0; directoryIndex < 40; directoryIndex++) {
|
||||
`;
|
||||
|
||||
describe("CLI filesystem watcher with a real filesystem", () => {
|
||||
it.each([
|
||||
{ name: "an ordinary Vault", parent: "notes", vault: "vault", alias: false, resolveAlias: true },
|
||||
{
|
||||
name: "an ordinary Vault through a symlink",
|
||||
parent: "notes",
|
||||
vault: "vault",
|
||||
alias: true,
|
||||
resolveAlias: true,
|
||||
},
|
||||
{
|
||||
name: "a hidden-parent Vault through a preserved symlink",
|
||||
parent: ".obsidian",
|
||||
vault: "vault",
|
||||
alias: true,
|
||||
resolveAlias: false,
|
||||
},
|
||||
{
|
||||
name: "a hidden-parent Vault by physical path",
|
||||
parent: ".obsidian",
|
||||
vault: "vault",
|
||||
alias: false,
|
||||
resolveAlias: true,
|
||||
},
|
||||
{
|
||||
name: "a hidden-parent Vault through a resolved symlink",
|
||||
parent: ".obsidian",
|
||||
vault: "vault",
|
||||
alias: true,
|
||||
resolveAlias: true,
|
||||
},
|
||||
{ name: "a dot-prefixed Vault root", parent: "notes", vault: ".vault", alias: false, resolveAlias: true },
|
||||
{
|
||||
name: "a Vault below a database-like parent",
|
||||
parent: "archive-livesync-v2",
|
||||
vault: "vault",
|
||||
alias: false,
|
||||
resolveAlias: true,
|
||||
},
|
||||
{
|
||||
name: "a database-like Vault root",
|
||||
parent: "notes",
|
||||
vault: "vault-livesync-v2",
|
||||
alias: false,
|
||||
resolveAlias: true,
|
||||
},
|
||||
])(
|
||||
"keeps monitoring files in $name",
|
||||
async ({ parent, vault, alias, resolveAlias }) => {
|
||||
const fixture = await mkdtemp(join(tmpdir(), "livesync-watch-root-"));
|
||||
const physicalVault = join(fixture, parent, vault);
|
||||
const aliasPath = join(fixture, "livesync-vault");
|
||||
const events: WatchEvent[] = [];
|
||||
const diagnostics: unknown[] = [];
|
||||
await mkdir(physicalVault, { recursive: true });
|
||||
await symlink(physicalVault, aliasPath, "dir");
|
||||
const suppliedPath = alias ? aliasPath : physicalVault;
|
||||
const watchRoot = resolveAlias ? await realpath(suppliedPath) : suppliedPath;
|
||||
const adapter = new CLIStorageEventManagerAdapter(watchRoot, undefined, true, (message, detail) =>
|
||||
diagnostics.push({ message, detail })
|
||||
);
|
||||
|
||||
try {
|
||||
await adapter.watch.beginWatch(makeHandlers(events));
|
||||
const noteName = "new-note.md";
|
||||
const notePath = join(physicalVault, noteName);
|
||||
await writeFile(notePath, "created after watcher readiness");
|
||||
const created = await waitFor(() =>
|
||||
events.some((event) => event.type === "create" && !event.folder && event.path === noteName)
|
||||
);
|
||||
expect(await readFile(notePath, "utf8")).toBe("created after watcher readiness");
|
||||
expect(diagnostics).toEqual([]);
|
||||
expect(created, `Expected a new-file event below ${watchRoot}`).toBe(true);
|
||||
|
||||
await writeFile(notePath, "modified after receiving the creation event");
|
||||
expect(
|
||||
await waitFor(() => events.some((event) => event.type === "change" && event.path === noteName))
|
||||
).toBe(true);
|
||||
await unlink(notePath);
|
||||
expect(
|
||||
await waitFor(() => events.some((event) => event.type === "delete" && event.path === noteName))
|
||||
).toBe(true);
|
||||
} finally {
|
||||
await adapter.close();
|
||||
await rm(fixture, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
20_000
|
||||
);
|
||||
|
||||
it("preserves monitoring through a descendant symlink to another directory", async () => {
|
||||
const fixture = await mkdtemp(join(tmpdir(), "livesync-watch-child-link-"));
|
||||
const root = join(fixture, "vault");
|
||||
const target = join(fixture, "external");
|
||||
await mkdir(root);
|
||||
await mkdir(target);
|
||||
await symlink(target, join(root, "linked"), "dir");
|
||||
const events: WatchEvent[] = [];
|
||||
const adapter = new CLIStorageEventManagerAdapter(root, undefined, true);
|
||||
try {
|
||||
await adapter.watch.beginWatch(makeHandlers(events));
|
||||
await writeFile(join(target, "linked-note.md"), "written through the physical target");
|
||||
expect(
|
||||
await waitFor(() =>
|
||||
events.some((event) => event.type === "create" && event.path === "linked/linked-note.md")
|
||||
)
|
||||
).toBe(true);
|
||||
} finally {
|
||||
await adapter.close();
|
||||
await rm(fixture, { recursive: true, force: true });
|
||||
}
|
||||
}, 15_000);
|
||||
|
||||
it("reports all 400 files when another process creates directories before dot-temp renames", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "livesync-issue1328-burst-"));
|
||||
const events: WatchEvent[] = [];
|
||||
@@ -111,40 +223,48 @@ describe("CLI filesystem watcher with a real filesystem", () => {
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("ignores initial, hidden, and custom-ignored files while watching visible files", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "livesync-real-watch-ignore-"));
|
||||
const events: WatchEvent[] = [];
|
||||
const ignoreDirectory = join(root, ".livesync");
|
||||
await mkdir(ignoreDirectory);
|
||||
await writeFile(join(ignoreDirectory, "ignore"), "private/\n");
|
||||
const ignoreRules = new IgnoreRules(root);
|
||||
await ignoreRules.load();
|
||||
await mkdir(join(root, "private"));
|
||||
await writeFile(join(root, "present-before-watch.md"), "initial file");
|
||||
await writeFile(join(root, ".hidden-before-watch.md"), "initial hidden file");
|
||||
const adapter = new CLIStorageEventManagerAdapter(root, ignoreRules, true);
|
||||
it.each(["visible-parent", ".hidden-parent"])(
|
||||
"ignores hidden, internal, and custom-ignored entries below %s",
|
||||
async (parent) => {
|
||||
const fixture = await mkdtemp(join(tmpdir(), "livesync-real-watch-ignore-"));
|
||||
const root = join(fixture, parent, "vault");
|
||||
await mkdir(root, { recursive: true });
|
||||
const events: WatchEvent[] = [];
|
||||
const ignoreDirectory = join(root, ".livesync");
|
||||
await mkdir(ignoreDirectory);
|
||||
await writeFile(join(ignoreDirectory, "ignore"), "private/\n");
|
||||
const ignoreRules = new IgnoreRules(root);
|
||||
await ignoreRules.load();
|
||||
await mkdir(join(root, "private"));
|
||||
await mkdir(join(root, "local-livesync-v2"));
|
||||
await writeFile(join(root, "present-before-watch.md"), "initial file");
|
||||
await writeFile(join(root, ".hidden-before-watch.md"), "initial hidden file");
|
||||
const adapter = new CLIStorageEventManagerAdapter(root, ignoreRules, true);
|
||||
|
||||
try {
|
||||
await adapter.watch.beginWatch(makeHandlers(events));
|
||||
await writeFile(join(root, "visible-after-watch.md"), "visible");
|
||||
await writeFile(join(root, ".hidden-after-watch.md"), "hidden");
|
||||
await writeFile(join(root, "private", "ignored-after-watch.md"), "custom ignored");
|
||||
try {
|
||||
await adapter.watch.beginWatch(makeHandlers(events));
|
||||
await writeFile(join(root, "visible-after-watch.md"), "visible");
|
||||
await writeFile(join(root, ".hidden-after-watch.md"), "hidden");
|
||||
await writeFile(join(root, "private", "ignored-after-watch.md"), "custom ignored");
|
||||
await writeFile(join(root, "local-livesync-v2", "state"), "internal database");
|
||||
|
||||
expect(
|
||||
await waitFor(() =>
|
||||
events.some((event) => event.type === "create" && event.path === "visible-after-watch.md")
|
||||
)
|
||||
).toBe(true);
|
||||
await delay(700);
|
||||
expect(
|
||||
await waitFor(() =>
|
||||
events.some((event) => event.type === "create" && event.path === "visible-after-watch.md")
|
||||
)
|
||||
).toBe(true);
|
||||
await delay(700);
|
||||
|
||||
expect(events.filter((event) => !event.folder)).toEqual([
|
||||
{ type: "create", path: "visible-after-watch.md", folder: false },
|
||||
]);
|
||||
} finally {
|
||||
await adapter.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 15_000);
|
||||
expect(events.filter((event) => !event.folder)).toEqual([
|
||||
{ type: "create", path: "visible-after-watch.md", folder: false },
|
||||
]);
|
||||
} finally {
|
||||
await adapter.close();
|
||||
await rm(fixture, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
15_000
|
||||
);
|
||||
|
||||
it("reports changes, deletions, rename paths, and stops after close", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "livesync-real-watch-lifecycle-"));
|
||||
|
||||
Reference in new issue
Block a user