power loss

This commit is contained in:
Andras Schmelczer 2026-09-21 21:29:52 +01:00
commit d56084e4bc
2 changed files with 18667 additions and 3902 deletions

View file

@ -51,7 +51,7 @@ test("NodeFileSystemOperations - exists with forward slashes", async () => {
}
});
test("NodeFileSystemOperations - delete with forward slashes", async () => {
test("NodeFileSystemOperations - deleteFile with forward slashes", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "vaultlink-test-"));
const fsOps = new NodeFileSystemOperations(tempDir);
@ -59,8 +59,10 @@ test("NodeFileSystemOperations - delete with forward slashes", async () => {
await fsOps.write("test.txt", new TextEncoder().encode("test"));
assert.equal(await fsOps.exists("test.txt"), true);
await fsOps.delete("test.txt");
await fsOps.deleteFile("test.txt");
assert.equal(await fsOps.exists("test.txt"), false);
await fsOps.deleteFile("test.txt");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
@ -154,9 +156,46 @@ test("NodeFileSystemOperations - handles paths with forward slashes on all platf
const readContent = await fsOps.read(testPath);
assert.equal(new TextDecoder().decode(readContent), "test");
await fsOps.delete(testPath);
await fsOps.deleteFile(testPath);
assert.equal(await fsOps.exists(testPath), false);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
test("NodeFileSystemOperations - delete prunes empty directory trees", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "vaultlink-test-"));
const fsOps = new NodeFileSystemOperations(tempDir);
try {
await fs.mkdir(path.join(tempDir, "parent", "child"), {
recursive: true
});
await fsOps.delete("parent");
assert.equal(await fsOps.exists("parent"), false);
await fsOps.delete("parent");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
test("NodeFileSystemOperations - delete does not remove files", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "vaultlink-test-"));
const fsOps = new NodeFileSystemOperations(tempDir);
try {
await fsOps.write(
"parent/file.txt",
new TextEncoder().encode("preserve me")
);
await assert.rejects(async () => fsOps.delete("parent"));
assert.equal(await fsOps.exists("parent/file.txt"), true);
await assert.rejects(async () => fsOps.delete("parent/file.txt"));
assert.equal(await fsOps.exists("parent/file.txt"), true);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});

View file

@ -15,7 +15,7 @@ import { toUnixPath } from "./path-utils";
export const VAULTLINK_DIR = ".vaultlink";
export class NodeFileSystemOperations implements FileSystemOperations {
public constructor(private readonly basePath: string) { }
public constructor(private readonly basePath: string) {}
public async listFilesRecursively(
directory: RelativePath | undefined
@ -105,9 +105,36 @@ export class NodeFileSystemOperations implements FileSystemOperations {
}
public async delete(relativePath: RelativePath): Promise<void> {
if (!relativePath) {
throw new Error("Cannot delete the vault root");
}
const fullPath = path.join(this.basePath, relativePath);
try {
await fs.unlink(fullPath);
await this.deleteDirectoryTree(fullPath);
} catch (error) {
throw new Error(
`Failed to delete directory ${fullPath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
public async deleteFile(relativePath: RelativePath): Promise<void> {
const fullPath = path.join(this.basePath, relativePath);
try {
const entry = await fs.lstat(fullPath).catch((error: unknown) => {
if (this.isMissing(error)) return undefined;
throw error;
});
if (!entry) return;
if (!entry.isFile() || entry.nlink !== 1) {
throw new Error("Cannot unlink a non-regular or linked file");
}
await fs.unlink(fullPath).catch((error: unknown) => {
if (!this.isMissing(error)) throw error;
});
await this.syncDirectory(path.dirname(fullPath));
} catch (error) {
throw new Error(
`Failed to delete file ${fullPath}: ${error instanceof Error ? error.message : String(error)}`
@ -171,6 +198,40 @@ export class NodeFileSystemOperations implements FileSystemOperations {
}
}
private async deleteDirectoryTree(fullPath: string): Promise<void> {
const entry = await fs.lstat(fullPath).catch((error: unknown) => {
if (this.isMissing(error)) return undefined;
throw error;
});
if (!entry) return;
if (!entry.isDirectory()) {
throw new Error("Cannot delete a regular file as a directory");
}
for (const child of await fs.readdir(fullPath, {
withFileTypes: true
})) {
if (!child.isDirectory()) {
throw new Error(
`Directory contains a non-directory entry: ${path.join(fullPath, child.name)}`
);
}
await this.deleteDirectoryTree(path.join(fullPath, child.name));
}
await fs.rmdir(fullPath);
await this.syncDirectory(path.dirname(fullPath));
}
private isMissing(error: unknown): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === "ENOENT"
);
}
private async walkDirectory(
relativePath: string,
files: RelativePath[]