@vrtmrz/livesync-commonlib 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/dist/API/DirectFileManipulatorV2.d.ts +1 -0
- package/dist/API/DirectFileManipulatorV2.js +6 -12
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/common/models/setting.const.defaults.js +1 -0
- package/dist/common/models/setting.const.qr.js +1 -0
- package/dist/common/models/setting.policy.d.ts +4 -0
- package/dist/common/models/setting.policy.js +1 -0
- package/dist/common/models/setting.type.d.ts +2 -0
- package/dist/common/models/tweak.definition.d.ts +1 -0
- package/dist/common/pathNamespace.d.ts +4 -0
- package/dist/common/pathNamespace.js +10 -0
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/settingConstants.js +4 -0
- package/dist/platform/AmbiguousNormalisedPathError.d.ts +16 -0
- package/dist/platform/AmbiguousNormalisedPathError.js +21 -0
- package/dist/platform/StoragePathResolver.d.ts +15 -0
- package/dist/platform/StoragePathResolver.js +113 -0
- package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.d.ts +74 -0
- package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.js +169 -0
- package/dist/platform/browser/index.d.ts +1 -0
- package/dist/platform/browser/index.js +2 -0
- package/dist/platform/browser/storage.d.ts +23 -0
- package/dist/platform/browser/storage.js +138 -0
- package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.d.ts +79 -0
- package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.js +218 -0
- package/dist/platform/node/index.d.ts +1 -0
- package/dist/platform/node/index.js +2 -0
- package/dist/platform/node/storage.d.ts +30 -1
- package/dist/platform/node/storage.js +88 -0
- package/dist/platform/pathComparison.d.ts +53 -0
- package/dist/platform/pathComparison.js +39 -0
- package/dist/platform/pathTypes.d.ts +154 -0
- package/dist/platform/pathTypes.js +0 -0
- package/dist/platform/storagePathResolution.d.ts +13 -0
- package/dist/platform/storagePathResolution.js +9 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +5 -3
- package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
- package/dist/serviceModules/Rebuilder.js +2 -1
- package/dist/services/base/FileProcessingService.d.ts +2 -2
- package/dist/services/base/RemoteConnection.d.ts +2 -0
- package/dist/services/base/RemoteService.d.ts +1 -1
- package/dist/services/base/RemoteService.js +13 -5
- package/dist/services/implements/injectable/InjectableVaultService.d.ts +1 -1
- package/dist/services/lib/couchdbHttpCache.d.ts +5 -0
- package/dist/services/lib/couchdbHttpCache.js +36 -0
- package/dist/storage.d.ts +4 -0
- package/dist/storage.js +19 -0
- package/dist/string_and_binary/path.js +4 -7
- package/dist/worker/bgWorker.js +1 -1
- package/docs/adr/2026_11_shared_storage_path_resolution.md +73 -0
- package/docs/design_docs/storage_path_resolution.md +305 -0
- package/docs/development.md +11 -1
- package/docs/platform-storage.md +61 -0
- package/docs/releasing.md +25 -0
- package/docs/unicode-path-identity.md +48 -17
- package/package.json +9 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { UXDataWriteOptions, UXStat } from "../../common/types.js";
|
|
2
2
|
import type { IStorageAdapter } from "../../serviceModules/adapters/IStorageAdapter.js";
|
|
3
|
+
import type { DirectoryEntry, StoragePath } from "../pathTypes.js";
|
|
3
4
|
export type FileSystemAccessStorageStat = UXStat;
|
|
4
5
|
export interface CreateFileSystemAccessStorageOptions {
|
|
5
6
|
/** A directory handle already selected and authorised by the host application. */
|
|
@@ -12,10 +13,32 @@ export declare class FileSystemAccessStorageAdapter implements IStorageAdapter<F
|
|
|
12
13
|
private resolvePath;
|
|
13
14
|
private getFileHandle;
|
|
14
15
|
private getDirectoryHandle;
|
|
16
|
+
private getStrictDirectoryHandle;
|
|
17
|
+
private getStrictFileHandle;
|
|
18
|
+
private requireStrictFileHandle;
|
|
19
|
+
private getStrictWritableFileHandle;
|
|
15
20
|
private resolveWritablePath;
|
|
16
21
|
exists(path: string): Promise<boolean>;
|
|
17
22
|
trystat(path: string): Promise<FileSystemAccessStorageStat | null>;
|
|
18
23
|
stat(path: string): Promise<FileSystemAccessStorageStat | null>;
|
|
24
|
+
/** Enumerate one directory without converting permission or type errors into absence. */
|
|
25
|
+
readDirectory(path: StoragePath): Promise<readonly DirectoryEntry[] | null>;
|
|
26
|
+
/** Stat a path using its exact physical spelling and preserve non-missing errors. */
|
|
27
|
+
statResolved(path: StoragePath): Promise<FileSystemAccessStorageStat | null>;
|
|
28
|
+
/** Read an exact resolved file path while preserving missing, type, and permission errors. */
|
|
29
|
+
readResolved(path: StoragePath): Promise<string>;
|
|
30
|
+
/** Read an exact resolved file path as bytes while preserving host errors. */
|
|
31
|
+
readBinaryResolved(path: StoragePath): Promise<ArrayBuffer>;
|
|
32
|
+
/** Write an exact resolved file path, creating missing parent directories. */
|
|
33
|
+
writeResolved(path: StoragePath, data: string, _options?: UXDataWriteOptions): Promise<void>;
|
|
34
|
+
/** Write bytes to an exact resolved file path, creating missing parent directories. */
|
|
35
|
+
writeBinaryResolved(path: StoragePath, data: ArrayBuffer, _options?: UXDataWriteOptions): Promise<void>;
|
|
36
|
+
/** Append through strict reads and writes so denied reads cannot become truncating writes. */
|
|
37
|
+
appendResolved(path: StoragePath, data: string, options?: UXDataWriteOptions): Promise<void>;
|
|
38
|
+
/** Remove an exact resolved path, retaining the adapter's no-op behaviour when it is absent. */
|
|
39
|
+
removeResolved(path: StoragePath): Promise<void>;
|
|
40
|
+
/** Create an exact resolved directory path without hiding browser errors. */
|
|
41
|
+
mkdirResolved(path: StoragePath): Promise<void>;
|
|
19
42
|
mkdir(path: string): Promise<void>;
|
|
20
43
|
remove(path: string): Promise<void>;
|
|
21
44
|
read(path: string): Promise<string>;
|
|
@@ -40,6 +40,47 @@ class FileSystemAccessStorageAdapter {
|
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
async getStrictDirectoryHandle(path) {
|
|
44
|
+
validateStoragePath(path);
|
|
45
|
+
let currentHandle = this.rootHandle;
|
|
46
|
+
for (const part of path.split("/").filter((segment) => segment !== "")) {
|
|
47
|
+
try {
|
|
48
|
+
currentHandle = await currentHandle.getDirectoryHandle(part);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (isNamedError(error, "NotFoundError")) return null;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return currentHandle;
|
|
55
|
+
}
|
|
56
|
+
async getStrictFileHandle(path) {
|
|
57
|
+
validateStoragePath(path, false);
|
|
58
|
+
const parts = path.split("/").filter((part) => part !== "");
|
|
59
|
+
const name = parts.pop();
|
|
60
|
+
const dirHandle = await this.getStrictDirectoryHandle(parts.join("/"));
|
|
61
|
+
if (!dirHandle) return null;
|
|
62
|
+
try {
|
|
63
|
+
return await dirHandle.getFileHandle(name);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (isNamedError(error, "NotFoundError")) return null;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async requireStrictFileHandle(path) {
|
|
70
|
+
const handle = await this.getStrictFileHandle(path);
|
|
71
|
+
if (!handle) throw new Error(`File not found: ${path}`);
|
|
72
|
+
return handle;
|
|
73
|
+
}
|
|
74
|
+
async getStrictWritableFileHandle(path) {
|
|
75
|
+
validateStoragePath(path, false);
|
|
76
|
+
const parts = path.split("/").filter((part) => part !== "");
|
|
77
|
+
const name = parts.pop();
|
|
78
|
+
const parentPath = parts.join("/");
|
|
79
|
+
await this.mkdir(parentPath);
|
|
80
|
+
const dirHandle = await this.getStrictDirectoryHandle(parentPath);
|
|
81
|
+
if (!dirHandle) throw new Error(`Invalid path: ${path}`);
|
|
82
|
+
return await dirHandle.getFileHandle(name, { create: true });
|
|
83
|
+
}
|
|
43
84
|
async resolveWritablePath(path) {
|
|
44
85
|
validateStoragePath(path, false);
|
|
45
86
|
const parts = path.split("/").filter((part) => part !== "");
|
|
@@ -71,6 +112,100 @@ class FileSystemAccessStorageAdapter {
|
|
|
71
112
|
async stat(path) {
|
|
72
113
|
return await this.trystat(path);
|
|
73
114
|
}
|
|
115
|
+
/** Enumerate one directory without converting permission or type errors into absence. */
|
|
116
|
+
async readDirectory(path) {
|
|
117
|
+
const dirHandle = await this.getStrictDirectoryHandle(path);
|
|
118
|
+
if (!dirHandle) return null;
|
|
119
|
+
const entries = [];
|
|
120
|
+
for await (const [name, entry] of dirHandle.entries()) {
|
|
121
|
+
if (entry.kind === "directory" || entry.kind === "file") {
|
|
122
|
+
entries.push({ name, kind: entry.kind });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return entries;
|
|
126
|
+
}
|
|
127
|
+
/** Stat a path using its exact physical spelling and preserve non-missing errors. */
|
|
128
|
+
async statResolved(path) {
|
|
129
|
+
validateStoragePath(path);
|
|
130
|
+
if (path === "") {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
return { size: 0, mtime: now, ctime: now, type: "folder" };
|
|
133
|
+
}
|
|
134
|
+
const parts = path.split("/");
|
|
135
|
+
const name = parts.pop();
|
|
136
|
+
const parentHandle = await this.getStrictDirectoryHandle(parts.join("/"));
|
|
137
|
+
if (!parentHandle) return null;
|
|
138
|
+
let fileHandle;
|
|
139
|
+
try {
|
|
140
|
+
fileHandle = await parentHandle.getFileHandle(name);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (isNamedError(error, "NotFoundError")) return null;
|
|
143
|
+
if (!isNamedError(error, "TypeMismatchError")) throw error;
|
|
144
|
+
try {
|
|
145
|
+
await parentHandle.getDirectoryHandle(name);
|
|
146
|
+
} catch (directoryError) {
|
|
147
|
+
if (isNamedError(directoryError, "NotFoundError")) return null;
|
|
148
|
+
throw directoryError;
|
|
149
|
+
}
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
return { size: 0, mtime: now, ctime: now, type: "folder" };
|
|
152
|
+
}
|
|
153
|
+
const file = await fileHandle.getFile();
|
|
154
|
+
return {
|
|
155
|
+
size: file.size,
|
|
156
|
+
mtime: file.lastModified,
|
|
157
|
+
ctime: file.lastModified,
|
|
158
|
+
type: "file"
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** Read an exact resolved file path while preserving missing, type, and permission errors. */
|
|
162
|
+
async readResolved(path) {
|
|
163
|
+
const handle = await this.requireStrictFileHandle(path);
|
|
164
|
+
return await (await handle.getFile()).text();
|
|
165
|
+
}
|
|
166
|
+
/** Read an exact resolved file path as bytes while preserving host errors. */
|
|
167
|
+
async readBinaryResolved(path) {
|
|
168
|
+
const handle = await this.requireStrictFileHandle(path);
|
|
169
|
+
return await (await handle.getFile()).arrayBuffer();
|
|
170
|
+
}
|
|
171
|
+
/** Write an exact resolved file path, creating missing parent directories. */
|
|
172
|
+
async writeResolved(path, data, _options) {
|
|
173
|
+
const handle = await this.getStrictWritableFileHandle(path);
|
|
174
|
+
const writable = await handle.createWritable();
|
|
175
|
+
await writable.write(data);
|
|
176
|
+
await writable.close();
|
|
177
|
+
}
|
|
178
|
+
/** Write bytes to an exact resolved file path, creating missing parent directories. */
|
|
179
|
+
async writeBinaryResolved(path, data, _options) {
|
|
180
|
+
const handle = await this.getStrictWritableFileHandle(path);
|
|
181
|
+
const writable = await handle.createWritable();
|
|
182
|
+
await writable.write(data);
|
|
183
|
+
await writable.close();
|
|
184
|
+
}
|
|
185
|
+
/** Append through strict reads and writes so denied reads cannot become truncating writes. */
|
|
186
|
+
async appendResolved(path, data, options) {
|
|
187
|
+
const handle = await this.getStrictFileHandle(path);
|
|
188
|
+
const existing = handle ? await (await handle.getFile()).text() : "";
|
|
189
|
+
await this.writeResolved(path, existing + data, options);
|
|
190
|
+
}
|
|
191
|
+
/** Remove an exact resolved path, retaining the adapter's no-op behaviour when it is absent. */
|
|
192
|
+
async removeResolved(path) {
|
|
193
|
+
validateStoragePath(path, false);
|
|
194
|
+
const parts = path.split("/").filter((part) => part !== "");
|
|
195
|
+
const name = parts.pop();
|
|
196
|
+
const dirHandle = await this.getStrictDirectoryHandle(parts.join("/"));
|
|
197
|
+
if (!dirHandle) return;
|
|
198
|
+
try {
|
|
199
|
+
await dirHandle.removeEntry(name, { recursive: true });
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (isNamedError(error, "NotFoundError")) return;
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Create an exact resolved directory path without hiding browser errors. */
|
|
206
|
+
async mkdirResolved(path) {
|
|
207
|
+
await this.mkdir(path);
|
|
208
|
+
}
|
|
74
209
|
async mkdir(path) {
|
|
75
210
|
validateStoragePath(path);
|
|
76
211
|
let currentHandle = this.rootHandle;
|
|
@@ -131,6 +266,9 @@ function createFileSystemAccessStorage({
|
|
|
131
266
|
}) {
|
|
132
267
|
return new FileSystemAccessStorageAdapter(rootHandle);
|
|
133
268
|
}
|
|
269
|
+
function isNamedError(error, name) {
|
|
270
|
+
return typeof error === "object" && error !== null && "name" in error && error.name === name;
|
|
271
|
+
}
|
|
134
272
|
import { validateStoragePath as validateStoragePath2 } from "../storagePath.js";
|
|
135
273
|
export {
|
|
136
274
|
FileSystemAccessStorageAdapter,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { FilePath, StoragePath } from "../pathTypes.js";
|
|
2
|
+
import { NodeStorageAdapter } from "./storage.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve NFC-equivalent logical paths against current Node directory names.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Each operation snapshots Unicode and case settings. Version `0` delegates to
|
|
8
|
+
* the existing adapter; version `1` resolves names and rejects observed aliases
|
|
9
|
+
* before I/O. Physical spelling and the host adapter's mutation semantics remain
|
|
10
|
+
* unchanged. Resolution does not reserve paths against concurrent changes.
|
|
11
|
+
*/
|
|
12
|
+
export declare class UnicodeResolvingNodeStorageAdapter extends NodeStorageAdapter {
|
|
13
|
+
private readonly getPathNormalizationVersion;
|
|
14
|
+
private readonly isCaseInsensitive;
|
|
15
|
+
/**
|
|
16
|
+
* Bind a host-owned root and callbacks for each operation's comparison settings.
|
|
17
|
+
*
|
|
18
|
+
* @param rootPath - Host-selected storage directory.
|
|
19
|
+
* @param getPathNormalizationVersion - Supplies `0` or `1`; defaults to Legacy `0`.
|
|
20
|
+
* @param isCaseInsensitive - Whether comparisons ignore case; defaults to false.
|
|
21
|
+
*/
|
|
22
|
+
constructor(rootPath: string, getPathNormalizationVersion?: () => number, isCaseInsensitive?: () => boolean);
|
|
23
|
+
/** Whether the current settings select NFC path resolution. */
|
|
24
|
+
get isUnicodePathNormalizationEnabled(): boolean;
|
|
25
|
+
/** Read metadata for an entry already resolved or obtained from a storage listing. */
|
|
26
|
+
statExact(path: StoragePath): Promise<import("../../common/types.js").UXStat>;
|
|
27
|
+
/** Inspect a physical directory once before descending into its enumerated entries. */
|
|
28
|
+
listExact(path: StoragePath): Promise<{
|
|
29
|
+
files: StoragePath[];
|
|
30
|
+
folders: StoragePath[];
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a logical path under one snapshot of the current settings.
|
|
34
|
+
*
|
|
35
|
+
* @param path - Original root-relative file path.
|
|
36
|
+
* @param caseInsensitive - Override this lookup's case rule without changing settings.
|
|
37
|
+
* @param allowMissing - Keep a missing write suffix when true; defaults to true.
|
|
38
|
+
* @returns The physical target, or `null` for an absent path when `allowMissing`
|
|
39
|
+
* is false in NFC mode. Legacy mode returns the validated original path.
|
|
40
|
+
* @throws Propagates NFC ambiguity and reader failures, and rejects invalid input.
|
|
41
|
+
*/
|
|
42
|
+
resolveLogicalPath(path: FilePath, caseInsensitive?: boolean, allowMissing?: boolean): Promise<StoragePath | null>;
|
|
43
|
+
/** Check whether the logical path exists under the current Unicode policy. */
|
|
44
|
+
exists(path: string, caseInsensitive?: boolean): Promise<boolean>;
|
|
45
|
+
/** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
|
|
46
|
+
trystat(path: string, caseInsensitive?: boolean): Promise<import("../../common/types.js").UXStat>;
|
|
47
|
+
/** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
|
|
48
|
+
stat(path: string, caseInsensitive?: boolean): Promise<import("../../common/types.js").UXStat>;
|
|
49
|
+
/** Create a directory at the resolved write target. */
|
|
50
|
+
mkdir(path: string, caseInsensitive?: boolean): Promise<void>;
|
|
51
|
+
/** Remove the entry at its resolved physical path. */
|
|
52
|
+
remove(path: string, caseInsensitive?: boolean): Promise<void>;
|
|
53
|
+
/** Read text from the resolved physical file. */
|
|
54
|
+
read(path: string, caseInsensitive?: boolean): Promise<string>;
|
|
55
|
+
/** Read bytes from the resolved physical file. */
|
|
56
|
+
readBinary(path: string, caseInsensitive?: boolean): Promise<ArrayBuffer>;
|
|
57
|
+
/** Write text at the resolved target while retaining existing physical spelling. */
|
|
58
|
+
write(path: string, data: string, options?: Parameters<NodeStorageAdapter["write"]>[2], caseInsensitive?: boolean): Promise<void>;
|
|
59
|
+
/** Write bytes at the resolved target while retaining existing physical spelling. */
|
|
60
|
+
writeBinary(path: string, data: ArrayBuffer, options?: Parameters<NodeStorageAdapter["writeBinary"]>[2], caseInsensitive?: boolean): Promise<void>;
|
|
61
|
+
/** Append text at the resolved target using the host adapter semantics. */
|
|
62
|
+
append(path: string, data: string, options?: Parameters<NodeStorageAdapter["append"]>[2], caseInsensitive?: boolean): Promise<void>;
|
|
63
|
+
/** Resolve and perform an explicit rename, including a requested leaf spelling change. */
|
|
64
|
+
rename(sourcePath: string, targetPath: string, caseInsensitive?: boolean): Promise<void>;
|
|
65
|
+
/** List logical directory contents, checking equivalent child names in NFC mode. */
|
|
66
|
+
list(basePath: string, caseInsensitive?: boolean): Promise<{
|
|
67
|
+
files: string[];
|
|
68
|
+
folders: string[];
|
|
69
|
+
}>;
|
|
70
|
+
private statWithSettings;
|
|
71
|
+
private requireExistingPath;
|
|
72
|
+
private preflightRequestedPath;
|
|
73
|
+
private listPhysicalDirectory;
|
|
74
|
+
private createResolver;
|
|
75
|
+
private getSettings;
|
|
76
|
+
private getPathNormalizationVersionSnapshot;
|
|
77
|
+
private asStoragePath;
|
|
78
|
+
private missingPathError;
|
|
79
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { getPathNormalizationVersion as getSupportedPathNormalizationVersion } from "../../common/pathNormalization.js";
|
|
2
|
+
import { createStoragePathResolver } from "../storagePathResolution.js";
|
|
3
|
+
import { parseFilePath } from "../pathComparison.js";
|
|
4
|
+
import { NodeStorageAdapter } from "./storage.js";
|
|
5
|
+
class UnicodeResolvingNodeStorageAdapter extends NodeStorageAdapter {
|
|
6
|
+
/**
|
|
7
|
+
* Bind a host-owned root and callbacks for each operation's comparison settings.
|
|
8
|
+
*
|
|
9
|
+
* @param rootPath - Host-selected storage directory.
|
|
10
|
+
* @param getPathNormalizationVersion - Supplies `0` or `1`; defaults to Legacy `0`.
|
|
11
|
+
* @param isCaseInsensitive - Whether comparisons ignore case; defaults to false.
|
|
12
|
+
*/
|
|
13
|
+
constructor(rootPath, getPathNormalizationVersion = () => 0, isCaseInsensitive = () => false) {
|
|
14
|
+
super(rootPath);
|
|
15
|
+
this.getPathNormalizationVersion = getPathNormalizationVersion;
|
|
16
|
+
this.isCaseInsensitive = isCaseInsensitive;
|
|
17
|
+
}
|
|
18
|
+
getPathNormalizationVersion;
|
|
19
|
+
isCaseInsensitive;
|
|
20
|
+
/** Whether the current settings select NFC path resolution. */
|
|
21
|
+
get isUnicodePathNormalizationEnabled() {
|
|
22
|
+
return this.getPathNormalizationVersionSnapshot() === 1;
|
|
23
|
+
}
|
|
24
|
+
/** Read metadata for an entry already resolved or obtained from a storage listing. */
|
|
25
|
+
async statExact(path) {
|
|
26
|
+
const settings = this.getSettings();
|
|
27
|
+
if (settings.pathNormalizationVersion === 0) return await super.trystat(path);
|
|
28
|
+
return await super.statResolved(this.asStoragePath(path));
|
|
29
|
+
}
|
|
30
|
+
/** Inspect a physical directory once before descending into its enumerated entries. */
|
|
31
|
+
async listExact(path) {
|
|
32
|
+
const settings = this.getSettings();
|
|
33
|
+
if (settings.pathNormalizationVersion === 0) {
|
|
34
|
+
const entries = await super.list(path);
|
|
35
|
+
return {
|
|
36
|
+
files: entries.files.map((entry) => this.asStoragePath(entry)),
|
|
37
|
+
folders: entries.folders.map((entry) => this.asStoragePath(entry))
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return await this.listPhysicalDirectory(this.asStoragePath(path), settings);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a logical path under one snapshot of the current settings.
|
|
44
|
+
*
|
|
45
|
+
* @param path - Original root-relative file path.
|
|
46
|
+
* @param caseInsensitive - Override this lookup's case rule without changing settings.
|
|
47
|
+
* @param allowMissing - Keep a missing write suffix when true; defaults to true.
|
|
48
|
+
* @returns The physical target, or `null` for an absent path when `allowMissing`
|
|
49
|
+
* is false in NFC mode. Legacy mode returns the validated original path.
|
|
50
|
+
* @throws Propagates NFC ambiguity and reader failures, and rejects invalid input.
|
|
51
|
+
*/
|
|
52
|
+
async resolveLogicalPath(path, caseInsensitive, allowMissing = true) {
|
|
53
|
+
const settings = this.getSettings(caseInsensitive);
|
|
54
|
+
const filePath = parseFilePath(path);
|
|
55
|
+
if (settings.pathNormalizationVersion === 0) return filePath;
|
|
56
|
+
await this.preflightRequestedPath(filePath);
|
|
57
|
+
const resolver = this.createResolver(settings);
|
|
58
|
+
return allowMissing ? await resolver.resolveForWrite(filePath) : await resolver.resolveExisting(filePath);
|
|
59
|
+
}
|
|
60
|
+
/** Check whether the logical path exists under the current Unicode policy. */
|
|
61
|
+
async exists(path, caseInsensitive) {
|
|
62
|
+
const settings = this.getSettings(caseInsensitive);
|
|
63
|
+
if (settings.pathNormalizationVersion === 0) return await super.exists(path);
|
|
64
|
+
await this.preflightRequestedPath(path);
|
|
65
|
+
const resolver = this.createResolver(settings);
|
|
66
|
+
const resolvedPath = await resolver.resolveExisting(parseFilePath(path));
|
|
67
|
+
if (resolvedPath === null) return false;
|
|
68
|
+
return await super.statResolved(resolvedPath) !== null;
|
|
69
|
+
}
|
|
70
|
+
/** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
|
|
71
|
+
async trystat(path, caseInsensitive) {
|
|
72
|
+
const settings = this.getSettings(caseInsensitive);
|
|
73
|
+
return await this.statWithSettings(path, settings);
|
|
74
|
+
}
|
|
75
|
+
/** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
|
|
76
|
+
async stat(path, caseInsensitive) {
|
|
77
|
+
const settings = this.getSettings(caseInsensitive);
|
|
78
|
+
return await this.statWithSettings(path, settings);
|
|
79
|
+
}
|
|
80
|
+
/** Create a directory at the resolved write target. */
|
|
81
|
+
async mkdir(path, caseInsensitive) {
|
|
82
|
+
const settings = this.getSettings(caseInsensitive);
|
|
83
|
+
if (settings.pathNormalizationVersion === 0) return await super.mkdir(path);
|
|
84
|
+
await this.preflightRequestedPath(path);
|
|
85
|
+
const resolvedPath = await this.createResolver(settings).resolveForWrite(parseFilePath(path));
|
|
86
|
+
await super.mkdirResolved(resolvedPath);
|
|
87
|
+
}
|
|
88
|
+
/** Remove the entry at its resolved physical path. */
|
|
89
|
+
async remove(path, caseInsensitive) {
|
|
90
|
+
const settings = this.getSettings(caseInsensitive);
|
|
91
|
+
if (settings.pathNormalizationVersion === 0) return await super.remove(path);
|
|
92
|
+
const resolvedPath = await this.requireExistingPath(path, this.createResolver(settings));
|
|
93
|
+
await super.removeResolved(resolvedPath);
|
|
94
|
+
}
|
|
95
|
+
/** Read text from the resolved physical file. */
|
|
96
|
+
async read(path, caseInsensitive) {
|
|
97
|
+
const settings = this.getSettings(caseInsensitive);
|
|
98
|
+
if (settings.pathNormalizationVersion === 0) return await super.read(path);
|
|
99
|
+
const resolvedPath = await this.requireExistingPath(path, this.createResolver(settings));
|
|
100
|
+
return await super.readResolved(resolvedPath);
|
|
101
|
+
}
|
|
102
|
+
/** Read bytes from the resolved physical file. */
|
|
103
|
+
async readBinary(path, caseInsensitive) {
|
|
104
|
+
const settings = this.getSettings(caseInsensitive);
|
|
105
|
+
if (settings.pathNormalizationVersion === 0) return await super.readBinary(path);
|
|
106
|
+
const resolvedPath = await this.requireExistingPath(path, this.createResolver(settings));
|
|
107
|
+
return await super.readBinaryResolved(resolvedPath);
|
|
108
|
+
}
|
|
109
|
+
/** Write text at the resolved target while retaining existing physical spelling. */
|
|
110
|
+
async write(path, data, options, caseInsensitive) {
|
|
111
|
+
const settings = this.getSettings(caseInsensitive);
|
|
112
|
+
if (settings.pathNormalizationVersion === 0) return await super.write(path, data, options);
|
|
113
|
+
await this.preflightRequestedPath(path);
|
|
114
|
+
const resolvedPath = await this.createResolver(settings).resolveForWrite(parseFilePath(path));
|
|
115
|
+
await super.writeResolved(resolvedPath, data, options);
|
|
116
|
+
}
|
|
117
|
+
/** Write bytes at the resolved target while retaining existing physical spelling. */
|
|
118
|
+
async writeBinary(path, data, options, caseInsensitive) {
|
|
119
|
+
const settings = this.getSettings(caseInsensitive);
|
|
120
|
+
if (settings.pathNormalizationVersion === 0) return await super.writeBinary(path, data, options);
|
|
121
|
+
await this.preflightRequestedPath(path);
|
|
122
|
+
const resolvedPath = await this.createResolver(settings).resolveForWrite(parseFilePath(path));
|
|
123
|
+
await super.writeBinaryResolved(resolvedPath, data, options);
|
|
124
|
+
}
|
|
125
|
+
/** Append text at the resolved target using the host adapter semantics. */
|
|
126
|
+
async append(path, data, options, caseInsensitive) {
|
|
127
|
+
const settings = this.getSettings(caseInsensitive);
|
|
128
|
+
if (settings.pathNormalizationVersion === 0) return await super.append(path, data, options);
|
|
129
|
+
await this.preflightRequestedPath(path);
|
|
130
|
+
const resolvedPath = await this.createResolver(settings).resolveForWrite(parseFilePath(path));
|
|
131
|
+
await super.appendResolved(resolvedPath, data, options);
|
|
132
|
+
}
|
|
133
|
+
/** Resolve and perform an explicit rename, including a requested leaf spelling change. */
|
|
134
|
+
async rename(sourcePath, targetPath, caseInsensitive) {
|
|
135
|
+
const settings = this.getSettings(caseInsensitive);
|
|
136
|
+
if (settings.pathNormalizationVersion === 0) return await super.rename(sourcePath, targetPath);
|
|
137
|
+
await this.preflightRequestedPath(sourcePath, false);
|
|
138
|
+
await this.preflightRequestedPath(targetPath, false);
|
|
139
|
+
const resolver = this.createResolver(settings);
|
|
140
|
+
const resolved = await resolver.resolveRename(parseFilePath(sourcePath), parseFilePath(targetPath));
|
|
141
|
+
if (resolved === null) throw this.missingPathError(sourcePath);
|
|
142
|
+
await super.renameResolved(resolved.source, resolved.target);
|
|
143
|
+
}
|
|
144
|
+
/** List logical directory contents, checking equivalent child names in NFC mode. */
|
|
145
|
+
async list(basePath, caseInsensitive) {
|
|
146
|
+
const settings = this.getSettings(caseInsensitive);
|
|
147
|
+
if (settings.pathNormalizationVersion === 0) return await super.list(basePath);
|
|
148
|
+
await this.preflightRequestedPath(basePath);
|
|
149
|
+
const resolver = this.createResolver(settings);
|
|
150
|
+
const resolvedPath = await resolver.resolveExisting(parseFilePath(basePath));
|
|
151
|
+
if (resolvedPath === null) return { files: [], folders: [] };
|
|
152
|
+
return await this.listPhysicalDirectory(resolvedPath, settings, resolver);
|
|
153
|
+
}
|
|
154
|
+
async statWithSettings(path, settings) {
|
|
155
|
+
if (settings.pathNormalizationVersion === 0) return await super.trystat(path);
|
|
156
|
+
await this.preflightRequestedPath(path);
|
|
157
|
+
const resolvedPath = await this.createResolver(settings).resolveExisting(parseFilePath(path));
|
|
158
|
+
if (resolvedPath === null) return null;
|
|
159
|
+
return await super.statResolved(resolvedPath);
|
|
160
|
+
}
|
|
161
|
+
async requireExistingPath(path, resolver) {
|
|
162
|
+
await this.preflightRequestedPath(path);
|
|
163
|
+
const resolvedPath = await resolver.resolveExisting(parseFilePath(path));
|
|
164
|
+
if (resolvedPath === null) throw this.missingPathError(path);
|
|
165
|
+
return resolvedPath;
|
|
166
|
+
}
|
|
167
|
+
async preflightRequestedPath(path, allowRoot = true) {
|
|
168
|
+
try {
|
|
169
|
+
await this.resolvePath(path, allowRoot);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error.code === "ENOTDIR") return;
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async listPhysicalDirectory(path, settings, resolver = this.createResolver(settings)) {
|
|
176
|
+
const entries = await resolver.listResolvedDirectory(path);
|
|
177
|
+
const files = [];
|
|
178
|
+
const folders = [];
|
|
179
|
+
for (const entry of entries ?? []) {
|
|
180
|
+
if (entry.kind === "directory") folders.push(entry.path);
|
|
181
|
+
else files.push(entry.path);
|
|
182
|
+
}
|
|
183
|
+
return { files, folders };
|
|
184
|
+
}
|
|
185
|
+
createResolver(settings) {
|
|
186
|
+
return createStoragePathResolver({
|
|
187
|
+
readDirectory: async (path) => await super.readDirectory(path),
|
|
188
|
+
policy: {
|
|
189
|
+
pathNormalizationVersion: 1,
|
|
190
|
+
handleFilenameCaseSensitive: !settings.caseInsensitive
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
getSettings(caseInsensitive) {
|
|
195
|
+
const pathNormalizationVersion = this.getPathNormalizationVersionSnapshot();
|
|
196
|
+
return {
|
|
197
|
+
pathNormalizationVersion,
|
|
198
|
+
caseInsensitive: caseInsensitive ?? this.isCaseInsensitive()
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
getPathNormalizationVersionSnapshot() {
|
|
202
|
+
return getSupportedPathNormalizationVersion({
|
|
203
|
+
pathNormalizationVersion: this.getPathNormalizationVersion()
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
asStoragePath(path) {
|
|
207
|
+
return parseFilePath(path);
|
|
208
|
+
}
|
|
209
|
+
missingPathError(path) {
|
|
210
|
+
const error = new Error("ENOENT: no such file or directory, " + path);
|
|
211
|
+
error.code = "ENOENT";
|
|
212
|
+
error.path = path;
|
|
213
|
+
return error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export {
|
|
217
|
+
UnicodeResolvingNodeStorageAdapter
|
|
218
|
+
};
|
|
@@ -20,5 +20,6 @@ export declare const readline: typeof nodeReadlinePromises;
|
|
|
20
20
|
export declare const builtinModules: readonly string[];
|
|
21
21
|
/** Returns whether a module specifier refers to a Node built-in. */
|
|
22
22
|
export declare const isBuiltin: typeof nodeIsBuiltin;
|
|
23
|
+
export { UnicodeResolvingNodeStorageAdapter } from "./UnicodeResolvingNodeStorageAdapter.js";
|
|
23
24
|
export { NodeStorageAdapter, createNodeStorage, getNodeFileCreationTime, validateStoragePath, type CreateNodeStorageOptions, type NodeStorageStat, } from "./storage.js";
|
|
24
25
|
export { createNodeStandardIo, type CreateNodeStandardIoOptions, type NodeStandardInput, type NodeStandardOutput, } from "./standardIo.js";
|
|
@@ -12,6 +12,7 @@ const os = nodeOs;
|
|
|
12
12
|
const readline = nodeReadlinePromises;
|
|
13
13
|
const builtinModules = nodeBuiltinModules;
|
|
14
14
|
const isBuiltin = nodeIsBuiltin;
|
|
15
|
+
import { UnicodeResolvingNodeStorageAdapter } from "./UnicodeResolvingNodeStorageAdapter.js";
|
|
15
16
|
import {
|
|
16
17
|
NodeStorageAdapter,
|
|
17
18
|
createNodeStorage,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
} from "./standardIo.js";
|
|
24
25
|
export {
|
|
25
26
|
NodeStorageAdapter,
|
|
27
|
+
UnicodeResolvingNodeStorageAdapter,
|
|
26
28
|
builtinModules,
|
|
27
29
|
createNodeStandardIo,
|
|
28
30
|
createNodeStorage,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Stats } from "node:fs";
|
|
2
2
|
import type { UXDataWriteOptions, UXStat } from "../../common/types.js";
|
|
3
3
|
import type { IStorageAdapter } from "../../serviceModules/adapters/IStorageAdapter.js";
|
|
4
|
+
import type { DirectoryEntry, StoragePath } from "../pathTypes.js";
|
|
4
5
|
export type NodeStorageStat = UXStat;
|
|
5
6
|
/** Read creation time in integer milliseconds, using change time when birth time is unavailable. */
|
|
6
7
|
export declare function getNodeFileCreationTime(stat: Pick<Stats, "birthtimeMs" | "ctimeMs">): number;
|
|
@@ -17,20 +18,48 @@ export interface CreateNodeStorageOptions {
|
|
|
17
18
|
export declare class NodeStorageAdapter implements IStorageAdapter<NodeStorageStat> {
|
|
18
19
|
private readonly rootPath;
|
|
19
20
|
constructor(rootPath: string);
|
|
20
|
-
|
|
21
|
+
protected resolvePath(path: string, allowRoot?: boolean): Promise<string>;
|
|
21
22
|
private assertNoSymbolicLinks;
|
|
22
23
|
exists(path: string): Promise<boolean>;
|
|
23
24
|
trystat(path: string): Promise<NodeStorageStat | null>;
|
|
25
|
+
/** Read metadata for an exact physical path, returning null only when it is absent. */
|
|
26
|
+
statResolved(path: StoragePath): Promise<NodeStorageStat | null>;
|
|
27
|
+
/** Enumerate immediate physical children, returning null only when the directory is absent. */
|
|
28
|
+
readDirectory(path: StoragePath): Promise<readonly DirectoryEntry[] | null>;
|
|
24
29
|
stat(path: string): Promise<NodeStorageStat | null>;
|
|
25
30
|
mkdir(path: string): Promise<void>;
|
|
31
|
+
/** Create an exact resolved directory path. */
|
|
32
|
+
mkdirResolved(path: StoragePath): Promise<void>;
|
|
33
|
+
private mkdirPath;
|
|
26
34
|
remove(path: string): Promise<void>;
|
|
35
|
+
/** Remove an exact resolved file or directory path. */
|
|
36
|
+
removeResolved(path: StoragePath): Promise<void>;
|
|
37
|
+
private removePath;
|
|
27
38
|
read(path: string): Promise<string>;
|
|
39
|
+
/** Read a file by its exact resolved physical path. */
|
|
40
|
+
readResolved(path: StoragePath): Promise<string>;
|
|
41
|
+
private readPath;
|
|
28
42
|
readBinary(path: string): Promise<ArrayBuffer>;
|
|
43
|
+
/** Read bytes by their exact resolved physical path. */
|
|
44
|
+
readBinaryResolved(path: StoragePath): Promise<ArrayBuffer>;
|
|
45
|
+
private readBinaryPath;
|
|
29
46
|
write(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
|
|
47
|
+
/** Write text to an exact resolved physical path. */
|
|
48
|
+
writeResolved(path: StoragePath, data: string, options?: UXDataWriteOptions): Promise<void>;
|
|
49
|
+
private writePath;
|
|
30
50
|
writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void>;
|
|
51
|
+
/** Write bytes to an exact resolved physical path. */
|
|
52
|
+
writeBinaryResolved(path: StoragePath, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void>;
|
|
53
|
+
private writeBinaryPath;
|
|
31
54
|
append(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
|
|
55
|
+
/** Append text to an exact resolved physical path. */
|
|
56
|
+
appendResolved(path: StoragePath, data: string, options?: UXDataWriteOptions): Promise<void>;
|
|
57
|
+
private appendPath;
|
|
32
58
|
/** Atomically rename or move an entry without leaving the injected root. */
|
|
33
59
|
rename(sourcePath: string, targetPath: string): Promise<void>;
|
|
60
|
+
/** Rename or move exact resolved physical paths without leaving the injected root. */
|
|
61
|
+
renameResolved(sourcePath: StoragePath, targetPath: StoragePath): Promise<void>;
|
|
62
|
+
private renamePath;
|
|
34
63
|
list(basePath: string): Promise<{
|
|
35
64
|
files: string[];
|
|
36
65
|
folders: string[];
|