mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-06 16:28:18 +00:00
Adopt Commonlib storage path contracts
Use the shared Node adapter and retain one comparison policy throughout each CLI scan. Propagate permission and I/O failures instead of reporting absence or a successful partial scan. Use shared namespace and comparison helpers in Obsidian while preserving its Vault APIs and alias policy. Cover native spelling and host filename compatibility, and document the dependency and integration boundaries.
This commit is contained in:
1 parent
2716d68b3d
commit
d557298ceb
13 files changed
+301
-272
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import { getStoragePathComparisonKey, type FilePath, type PathComparisonKey } from "@vrtmrz/livesync-commonlib/storage";
|
||||
|
||||
/**
|
||||
* Keeps storage from being changed through a name which the file system resolves but no directory lists.
|
||||
*
|
||||
@@ -20,7 +22,13 @@ export interface PathAliasGuardHost {
|
||||
/** How long the names which a directory listed are taken as still listed. A name which is missed is listed afresh. */
|
||||
const LISTING_LIFETIME_MS = 1_000;
|
||||
|
||||
const foldName = (name: string) => name.normalize("NFC").toLowerCase();
|
||||
// Host alias checks retain their fixed NFC and case-insensitive policy in both identity modes.
|
||||
// Original host names keep the host vocabulary; comparison does not apply rooted-storage input validation.
|
||||
const foldName = (name: string) =>
|
||||
getStoragePathComparisonKey(name as FilePath, {
|
||||
pathNormalizationVersion: 1,
|
||||
handleFilenameCaseSensitive: false,
|
||||
});
|
||||
|
||||
const nameOf = (entry: string) => {
|
||||
const segments = entry.split("/").filter((segment) => segment !== "");
|
||||
@@ -28,14 +36,14 @@ const nameOf = (entry: string) => {
|
||||
};
|
||||
|
||||
export class PathAliasGuard {
|
||||
private readonly listings = new Map<string, { names: Set<string>; listedAt: number }>();
|
||||
private readonly listings = new Map<string, { names: Set<PathComparisonKey<"storage">>; listedAt: number }>();
|
||||
|
||||
constructor(
|
||||
private readonly host: PathAliasGuardHost,
|
||||
private readonly now: () => number = Date.now
|
||||
) {}
|
||||
|
||||
private recentListing(directory: string): Set<string> | undefined {
|
||||
private recentListing(directory: string): Set<PathComparisonKey<"storage">> | undefined {
|
||||
const listing = this.listings.get(directory);
|
||||
if (!listing) return undefined;
|
||||
if (this.now() - listing.listedAt <= LISTING_LIFETIME_MS) return listing.names;
|
||||
@@ -43,7 +51,7 @@ export class PathAliasGuard {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async listAfresh(directory: string): Promise<Set<string>> {
|
||||
private async listAfresh(directory: string): Promise<Set<PathComparisonKey<"storage">>> {
|
||||
const names = new Set((await this.host.list(directory)).map((entry) => foldName(nameOf(entry))));
|
||||
this.listings.set(directory, { names, listedAt: this.now() });
|
||||
return names;
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { App, TFile } from "obsidian";
|
||||
import { afterAll, describe, expect, it, vi } from "vitest";
|
||||
import { ObsidianStorageAdapter } from "./ObsidianStorageAdapter";
|
||||
import { ObsidianVaultAdapter } from "./ObsidianVaultAdapter";
|
||||
import { createPathAliasGuard } from "./pathAliasGuard";
|
||||
import { createPathAliasGuard, PathAliasGuard } from "./pathAliasGuard";
|
||||
|
||||
const REFUSAL = "Refusing a path which reaches an existing entry by a name its folder does not list.";
|
||||
const CONFIG_DIRECTORY = ".obsidian";
|
||||
@@ -141,6 +141,30 @@ describe("paths which resolve to an existing entry under an unlisted name", () =
|
||||
expect(calls.trashFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows NFC and case equivalents of names exposed by the host listing", async () => {
|
||||
const folder = "Café".normalize("NFD");
|
||||
const guard = new PathAliasGuard({
|
||||
isIndexed: () => false,
|
||||
exists: async () => true,
|
||||
list: async (path) => (path === "" ? [folder] : [`${folder}/Note.md`]),
|
||||
});
|
||||
|
||||
await expect(guard.assertNoAlias("CAFÉ/note.md")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it.each(["x:cache", "host\\name"])(
|
||||
"allows a listed target beside the unrelated host-listed name %s",
|
||||
async (unrelatedName) => {
|
||||
const guard = new PathAliasGuard({
|
||||
isIndexed: () => false,
|
||||
exists: async () => true,
|
||||
list: async () => [unrelatedName, "note.md"],
|
||||
});
|
||||
|
||||
await expect(guard.assertNoAlias("note.md")).resolves.toBeUndefined();
|
||||
}
|
||||
);
|
||||
|
||||
it("allows listed names, case-only differences, indexed paths, and new files", async () => {
|
||||
const { app, calls } = fakeApp();
|
||||
const adapter = new ObsidianStorageAdapter(app);
|
||||
|
||||
Reference in new issue
Block a user