mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-17 10:06:00 +00:00
fix: synchronise case-only file renames safely
This commit is contained in:
@@ -36,8 +36,27 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeF
|
||||
return this.path.normalisePath(p as string);
|
||||
}
|
||||
|
||||
private async hasExactPathCase(pathStr: string): Promise<boolean> {
|
||||
try {
|
||||
const segments = pathStr.split("/").filter((segment) => segment !== "");
|
||||
let currentPath = this.basePath;
|
||||
for (const segment of segments) {
|
||||
const entries = await fs.readdir(currentPath);
|
||||
if (!entries.includes(segment)) return false;
|
||||
currentPath = path.join(currentPath, segment);
|
||||
}
|
||||
return segments.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getAbstractFileByPath(p: FilePath | string): Promise<NodeFile | null> {
|
||||
const pathStr = this.normalisePath(p);
|
||||
if (!this.fileCache.has(pathStr) && !(await this.hasExactPathCase(pathStr))) {
|
||||
this.fileCache.delete(pathStr);
|
||||
return null;
|
||||
}
|
||||
return await this.refreshFile(pathStr);
|
||||
}
|
||||
|
||||
@@ -74,6 +93,15 @@ export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeF
|
||||
return Array.from(this.fileCache.values());
|
||||
}
|
||||
|
||||
async renameFile(file: NodeFile, newPath: string): Promise<NodeFile> {
|
||||
const oldPath = file.path;
|
||||
await this.vault.rename(file, newPath);
|
||||
this.fileCache.delete(oldPath);
|
||||
const renamedFile = await this.refreshFile(newPath);
|
||||
if (!renamedFile) throw new Error(`Could not find renamed file: ${newPath}`);
|
||||
return renamedFile;
|
||||
}
|
||||
|
||||
async statFromNative(file: NodeFile): Promise<UXStat> {
|
||||
return file.stat;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,13 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
||||
};
|
||||
}
|
||||
|
||||
async rename(file: NodeFile, newPath: string): Promise<void> {
|
||||
const targetPath = this.resolvePath(newPath);
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.rename(this.resolvePath(file.path), targetPath);
|
||||
file.path = newPath as FilePath;
|
||||
}
|
||||
|
||||
async delete(file: NodeFile | NodeFolder, force = false): Promise<void> {
|
||||
const fullPath = this.resolvePath(file.path);
|
||||
const stat = await fs.stat(fullPath);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { FilePath } from "@lib/common/types";
|
||||
import { NodeFileSystemAdapter } from "./NodeFileSystemAdapter";
|
||||
import { NodeVaultAdapter } from "./NodeVaultAdapter";
|
||||
|
||||
describe("NodeVaultAdapter.rename", () => {
|
||||
it("changes the directory entry case without changing the content", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "livesync-case-rename-"));
|
||||
try {
|
||||
await writeFile(join(directory, "Calculus.md"), "content", "utf8");
|
||||
const adapter = new NodeVaultAdapter(directory);
|
||||
const file = {
|
||||
path: "Calculus.md" as FilePath,
|
||||
stat: { ctime: 1, mtime: 2, size: 7, type: "file" as const },
|
||||
};
|
||||
|
||||
await adapter.rename(file, "calculus.md");
|
||||
|
||||
expect(await readdir(directory)).toEqual(["calculus.md"]);
|
||||
expect(await readFile(join(directory, "calculus.md"), "utf8")).toBe("content");
|
||||
expect(file.path).toBe("calculus.md");
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("NodeFileSystemAdapter path case", () => {
|
||||
it("finds the stored case and refreshes the cache after a case-only rename", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "livesync-case-cache-"));
|
||||
try {
|
||||
await writeFile(join(directory, "Calculus.md"), "content", "utf8");
|
||||
const adapter = new NodeFileSystemAdapter(directory);
|
||||
|
||||
await expect(adapter.getAbstractFileByPath("calculus.md")).resolves.toBeNull();
|
||||
const existingFile = await adapter.getAbstractFileByPathInsensitive("calculus.md");
|
||||
expect(existingFile?.path).toBe("Calculus.md");
|
||||
if (!existingFile) throw new Error("Expected to find Calculus.md case-insensitively");
|
||||
const renamedFile = await adapter.renameFile(existingFile, "calculus.md");
|
||||
|
||||
expect(renamedFile.path).toBe("calculus.md");
|
||||
expect((await adapter.getFiles()).map((file) => file.path)).toEqual(["calculus.md"]);
|
||||
expect(await readdir(directory)).toEqual(["calculus.md"]);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user