139 lines
5.7 KiB
TypeScript
139 lines
5.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import * as fs from "node:fs/promises";
|
|
import * as path from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { NodeFileSystemOperations } from "../local-client-cli/src/node-filesystem";
|
|
import { Database, emptyState } from "../sync-client/src/persistence/database";
|
|
import { FileOperations } from "../sync-client/src/file-operations/file-operations";
|
|
import type { ServerConfig } from "../sync-client/src/services/server-config";
|
|
import { toStoredSnapshot } from "../sync-client/src/sync-operations/content";
|
|
|
|
test(
|
|
"real filesystem: direct updates, unsafe entries, and file swaps",
|
|
{ timeout: 20_000 },
|
|
async () => {
|
|
const directory = await fs.mkdtemp(
|
|
path.join(tmpdir(), "vault-link-real-fs-")
|
|
);
|
|
try {
|
|
const root = path.join(directory, "vault");
|
|
await fs.mkdir(root);
|
|
const disk = new NodeFileSystemOperations(root);
|
|
const snapshot = (text: string) => ({
|
|
content: new TextEncoder().encode(text)
|
|
});
|
|
await disk.write("updated.bin", snapshot("first"));
|
|
await disk.write("updated.bin", snapshot("second"));
|
|
assert.equal(
|
|
Buffer.from(
|
|
(await disk.read("updated.bin"))!.content
|
|
).toString(),
|
|
"second"
|
|
);
|
|
for (const unsafe of [
|
|
"../outside",
|
|
"/absolute",
|
|
"folder/../outside"
|
|
]) {
|
|
await assert.rejects(
|
|
disk.write(unsafe, snapshot("blocked")),
|
|
/Unsafe path/
|
|
);
|
|
await assert.rejects(disk.read(unsafe), /Unsafe path/);
|
|
}
|
|
// POSIX filenames can contain colons. The engine, not the adapter,
|
|
// allocates their portable names for sync.
|
|
if (process.platform !== "win32") {
|
|
await fs.writeFile(
|
|
path.join(root, "local:name.md"),
|
|
"local name"
|
|
);
|
|
assert.equal(
|
|
Buffer.from(
|
|
(await disk.read("local:name.md"))!.content
|
|
).toString(),
|
|
"local name"
|
|
);
|
|
await disk.rename("local:name.md", "portable-name.md");
|
|
}
|
|
await disk.write("a.md", snapshot("A"));
|
|
await disk.write("b.md", snapshot("B"));
|
|
// A file in an ancestor position makes descendants absent.
|
|
assert.equal(await disk.stat("a.md/absent.md"), undefined);
|
|
assert.equal(await disk.read("a.md/absent.md"), undefined);
|
|
await assert.rejects(disk.rename("a.md", "b.md"));
|
|
await assert.rejects(disk.deleteDirectory("a.md"), /regular file/);
|
|
await fs.symlink("a.md", path.join(root, "symlink"));
|
|
await assert.rejects(disk.read("symlink"), /Symlink/);
|
|
await fs.unlink(path.join(root, "symlink"));
|
|
await fs.link(path.join(root, "a.md"), path.join(root, "hardlink"));
|
|
await assert.rejects(disk.read("a.md"), /hardlink/);
|
|
await fs.unlink(path.join(root, "hardlink"));
|
|
// No-replace renames also protect case/normalization aliases.
|
|
for (const [original, alias] of [
|
|
["Case.bin", "case.bin"],
|
|
["é.bin", "e\u0301.bin"]
|
|
]) {
|
|
await disk.write(original, snapshot("original bytes"));
|
|
await disk.write("alias-source.bin", snapshot("alias bytes"));
|
|
if (await disk.exists(alias))
|
|
await assert.rejects(
|
|
disk.rename("alias-source.bin", alias)
|
|
);
|
|
else {
|
|
await disk.rename("alias-source.bin", alias);
|
|
assert.equal(
|
|
Buffer.from(
|
|
(await disk.read(alias))!.content
|
|
).toString(),
|
|
"alias bytes"
|
|
);
|
|
}
|
|
assert.equal(
|
|
Buffer.from(
|
|
(await disk.read(original))!.content
|
|
).toString(),
|
|
"original bytes"
|
|
);
|
|
}
|
|
const initial = emptyState("real");
|
|
initial.local = { a: "a.md", b: "b.md" };
|
|
for (const [id, relative] of Object.entries(initial.local))
|
|
initial.documents[id] = {
|
|
materialized: true,
|
|
observedHash: (
|
|
await toStoredSnapshot(
|
|
(await disk.read(relative))!
|
|
)
|
|
).hash
|
|
};
|
|
const database = new Database(
|
|
initial,
|
|
async () => { },
|
|
"real",
|
|
async () => initial
|
|
);
|
|
const operations = new FileOperations(disk, database, {
|
|
getConfig: async () => ({ mergeableFileExtensions: ["md"] })
|
|
} as ServerConfig);
|
|
const next = structuredClone(initial);
|
|
next.local = { a: "b.md", b: "a.md" };
|
|
await operations.apply(next);
|
|
assert.equal(
|
|
Buffer.from(
|
|
(await disk.read("a.md"))!.content
|
|
).toString(),
|
|
"B"
|
|
);
|
|
assert.equal(
|
|
Buffer.from(
|
|
(await disk.read("b.md"))!.content
|
|
).toString(),
|
|
"A"
|
|
);
|
|
} finally {
|
|
await fs.rm(directory, { recursive: true });
|
|
}
|
|
}
|
|
);
|