@vrtmrz/livesync-commonlib 0.2.3 → 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 +46 -8
- package/dist/API/DirectFileManipulatorV2.d.ts +8 -1
- package/dist/API/DirectFileManipulatorV2.js +52 -79
- package/dist/API/DirectFileWatchOwner.d.ts +24 -0
- package/dist/API/DirectFileWatchOwner.js +256 -0
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/API/processSetting.js +3 -3
- 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 +3 -1
- package/dist/common/models/shared.definition.configNames.js +1 -2
- package/dist/common/models/tweak.definition.d.ts +2 -1
- package/dist/common/pathNamespace.d.ts +4 -0
- package/dist/common/pathNamespace.js +10 -0
- package/dist/common/pathNormalization.d.ts +2 -0
- package/dist/common/pathNormalization.js +7 -2
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/settingConstants.js +4 -0
- package/dist/managers/ChunkFetcher.d.ts +3 -0
- package/dist/managers/ChunkFetcher.js +20 -13
- package/dist/managers/LiveSyncManagers.d.ts +2 -0
- package/dist/managers/LiveSyncManagers.js +1 -0
- package/dist/managers/chunkFetchSource.d.ts +14 -0
- package/dist/managers/chunkFetchSource.js +12 -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/pouchdb/LiveSyncDBFunctions.js +5 -0
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
- package/dist/pouchdb/LiveSyncLocalDB.js +13 -10
- package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
- package/dist/pouchdb/pathIdentityCompatibility.js +19 -1
- package/dist/replication/RemoteResource.d.ts +3 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +5 -3
- package/dist/replication/index.d.ts +2 -0
- package/dist/replication/index.js +2 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +1 -0
- package/dist/replication/journal/JournalSyncCore.js +35 -19
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +11 -3
- package/dist/replication/journal/LiveSyncJournalReplicator.js +123 -45
- package/dist/replication/trystero/P2PRoomSessionOwner.js +14 -13
- package/dist/replication/trystero/TrysteroReplicator.d.ts +9 -5
- package/dist/replication/trystero/TrysteroReplicator.js +98 -70
- package/dist/replication/trystero/TrysteroReplicator.pathIdentity.d.ts +11 -0
- package/dist/replication/trystero/TrysteroReplicator.pathIdentity.js +72 -0
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +2 -0
- package/dist/replication/trystero/useP2PReplicatorFeature.js +6 -2
- package/dist/serviceModules/Rebuilder.d.ts +1 -0
- package/dist/serviceModules/Rebuilder.js +35 -6
- 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/conflict-resolution.md +1 -1
- package/docs/design_docs/storage_path_resolution.md +305 -0
- package/docs/development.md +15 -1
- package/docs/platform-storage.md +61 -0
- package/docs/proven-in-use.md +30 -13
- package/docs/releasing.md +27 -0
- package/docs/service-feature-composition.md +7 -1
- package/docs/unicode-path-identity.md +104 -12
- package/package.json +10 -2
|
@@ -441,6 +441,10 @@ export declare const SETTINGS_PROPERTY_POLICY: {
|
|
|
441
441
|
readonly markdown: "include";
|
|
442
442
|
readonly persistence: "include";
|
|
443
443
|
};
|
|
444
|
+
readonly useCouchDBCacheControlHeadersOnly: {
|
|
445
|
+
readonly markdown: "include";
|
|
446
|
+
readonly persistence: "include";
|
|
447
|
+
};
|
|
444
448
|
readonly accessKey: {
|
|
445
449
|
readonly markdown: "credentials";
|
|
446
450
|
readonly persistence: "connection";
|
|
@@ -111,6 +111,7 @@ const SETTINGS_PROPERTY_POLICY = {
|
|
|
111
111
|
jwtSub: SettingPolicies.ConnectionCredentials,
|
|
112
112
|
jwtExpDuration: SettingPolicies.Default,
|
|
113
113
|
useRequestAPI: SettingPolicies.Default,
|
|
114
|
+
useCouchDBCacheControlHeadersOnly: SettingPolicies.Default,
|
|
114
115
|
accessKey: SettingPolicies.ConnectionCredentials,
|
|
115
116
|
secretKey: SettingPolicies.ConnectionCredentials,
|
|
116
117
|
bucket: SettingPolicies.LocalConnection,
|
|
@@ -38,6 +38,8 @@ export interface CouchDBConnection {
|
|
|
38
38
|
* Seems stable, so promoted to the normal setting.
|
|
39
39
|
*/
|
|
40
40
|
useRequestAPI: boolean;
|
|
41
|
+
/** Control CouchDB HTTP caching through headers alone. Defaults to false. */
|
|
42
|
+
useCouchDBCacheControlHeadersOnly?: boolean;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* Interface representing the settings for periodic replication.
|
|
@@ -557,7 +559,7 @@ export interface P2PConnectionInfo {
|
|
|
557
559
|
*/
|
|
558
560
|
P2P_useDiagRTC?: boolean;
|
|
559
561
|
}
|
|
560
|
-
export interface P2PSyncSetting extends P2PConnectionInfo {
|
|
562
|
+
export interface P2PSyncSetting extends P2PConnectionInfo, Partial<PathIdentitySettings> {
|
|
561
563
|
P2P_AutoAccepting: AutoAccepting;
|
|
562
564
|
P2P_AutoSyncPeers: string;
|
|
563
565
|
P2P_AutoWatchPeers: string;
|
|
@@ -66,8 +66,7 @@ const configurationNames = {
|
|
|
66
66
|
},
|
|
67
67
|
pathNormalizationVersion: {
|
|
68
68
|
name: "Path identity mode",
|
|
69
|
-
desc: "
|
|
70
|
-
isAdvanced: true
|
|
69
|
+
desc: "Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild from a complete authoritative Vault and Fetch on the other devices. With P2P, Rebuild prepares only the source device's local database. Resolve colliding file names first."
|
|
71
70
|
},
|
|
72
71
|
doNotUseFixedRevisionForChunks: {
|
|
73
72
|
name: "Content-derived chunk revisions (obsolete setting)",
|
|
@@ -120,8 +120,8 @@ export declare const TweakValuesRecommendedTemplate: Partial<ObsidianLiveSyncSet
|
|
|
120
120
|
export declare const TweakValuesDefault: Partial<ObsidianLiveSyncSettings>;
|
|
121
121
|
export declare const TweakValuesTemplate: {
|
|
122
122
|
tweakModified: number;
|
|
123
|
-
usePluginSyncV2: boolean;
|
|
124
123
|
pathNormalizationVersion: import("./setting.type.js").PathNormalizationVersion;
|
|
124
|
+
usePluginSyncV2: boolean;
|
|
125
125
|
encrypt: boolean;
|
|
126
126
|
idDerivationVersion: 0 | 1;
|
|
127
127
|
usePathObfuscation: boolean;
|
|
@@ -238,6 +238,7 @@ export declare const TweakValuesTemplate: {
|
|
|
238
238
|
jwtSub?: string;
|
|
239
239
|
jwtExpDuration?: number;
|
|
240
240
|
useRequestAPI?: boolean;
|
|
241
|
+
useCouchDBCacheControlHeadersOnly?: boolean;
|
|
241
242
|
accessKey?: string;
|
|
242
243
|
secretKey?: string;
|
|
243
244
|
bucket?: string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ICHeader, ICXHeader, PSCHeader } from "./models/fileaccess.const.js";
|
|
2
|
+
export type PathNamespace = "" | typeof ICHeader | typeof ICXHeader | typeof PSCHeader;
|
|
3
|
+
/** Separate one recognised Metadata namespace while retaining the body's spelling. */
|
|
4
|
+
export declare function splitPathNamespace(path: string): readonly [PathNamespace, string];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ICHeader, ICXHeader, PSCHeader } from "./models/fileaccess.const.js";
|
|
2
|
+
function splitPathNamespace(path) {
|
|
3
|
+
for (const prefix of [ICHeader, ICXHeader, PSCHeader]) {
|
|
4
|
+
if (path.startsWith(prefix)) return [prefix, path.slice(prefix.length)];
|
|
5
|
+
}
|
|
6
|
+
return ["", path];
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
splitPathNamespace
|
|
10
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { PathNormalizationVersion } from "./models/setting.type.js";
|
|
2
|
+
/** Reject unsupported path identity versions while allowing an absent legacy value. */
|
|
3
|
+
export declare function assertSupportedPathNormalizationVersion(version: number | undefined): asserts version is PathNormalizationVersion | undefined;
|
|
2
4
|
/** Resolve the stored path identity version, treating an absent legacy value as version zero. */
|
|
3
5
|
export declare function getPathNormalizationVersion(settings: {
|
|
4
6
|
pathNormalizationVersion?: number;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
function assertSupportedPathNormalizationVersion(version) {
|
|
2
|
+
if (version === void 0 || version === 0 || version === 1) return;
|
|
3
|
+
throw new Error(`Unsupported path normalisation version: ${version}`);
|
|
4
|
+
}
|
|
1
5
|
function getPathNormalizationVersion(settings) {
|
|
2
6
|
const version = settings.pathNormalizationVersion === void 0 ? 0 : settings.pathNormalizationVersion;
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
assertSupportedPathNormalizationVersion(version);
|
|
8
|
+
return version;
|
|
5
9
|
}
|
|
6
10
|
function normalisePathForIdentity(path, version) {
|
|
7
11
|
const resolvedVersion = getPathNormalizationVersion({ pathNormalizationVersion: version });
|
|
8
12
|
return resolvedVersion === 1 ? path.normalize("NFC") : path;
|
|
9
13
|
}
|
|
10
14
|
export {
|
|
15
|
+
assertSupportedPathNormalizationVersion,
|
|
11
16
|
getPathNormalizationVersion,
|
|
12
17
|
normalisePathForIdentity
|
|
13
18
|
};
|
|
@@ -375,6 +375,10 @@ const SettingInformation = {
|
|
|
375
375
|
name: "Use Request API to avoid `inevitable` CORS problem",
|
|
376
376
|
desc: "If enabled, the request API will be used to avoid `inevitable` CORS problems. This is a workaround and may not work in all cases. PLEASE READ THE DOCUMENTATION BEFORE USING THIS OPTION. This is a less-secure option."
|
|
377
377
|
},
|
|
378
|
+
useCouchDBCacheControlHeadersOnly: {
|
|
379
|
+
name: "Use headers only for HTTP cache control (compatibility)",
|
|
380
|
+
desc: "Remove the query parameter used to invalidate cached responses (the same URL behaviour as in 1.0.44 and earlier). Enable only if your CouchDB server or proxy rejects the additional cache query parameter. Older cached encrypted responses may then be reused after a remote Rebuild."
|
|
381
|
+
},
|
|
378
382
|
hideFileWarningNotice: {
|
|
379
383
|
name: "Show status icon instead of file warnings banner",
|
|
380
384
|
desc: "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown."
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DocumentID } from "../common/types.js";
|
|
2
2
|
import { type ChunkManager } from "./ChunkManager.js";
|
|
3
|
+
import type { ChunkFetchSource } from "./chunkFetchSource.js";
|
|
3
4
|
import type { IReplicatorService, ISettingService } from "../services/base/IService.js";
|
|
4
5
|
export declare const EVENT_MISSING_CHUNKS = "missingChunks";
|
|
5
6
|
export declare const EVENT_MISSING_CHUNK_REMOTE = "missingChunkRemote";
|
|
@@ -9,6 +10,8 @@ export type ChunkFetcherOptions = {
|
|
|
9
10
|
chunkManager: ChunkManager;
|
|
10
11
|
replicatorService: IReplicatorService;
|
|
11
12
|
deliveryStallTimeoutMs?: number;
|
|
13
|
+
/** Direct hosts replace both retrieval and acceptance; the default uses the active replicator. */
|
|
14
|
+
source?: ChunkFetchSource;
|
|
12
15
|
};
|
|
13
16
|
export declare class ChunkFetcher {
|
|
14
17
|
options: ChunkFetcherOptions;
|
|
@@ -327,20 +327,25 @@ class ChunkFetcher {
|
|
|
327
327
|
await this.resolveLocallyAvailableRetries(requestClaims);
|
|
328
328
|
let pendingIDs = await this.waitBeforeRemoteRequest(requestIDs, requestClaims, localCheckCompletionVersion);
|
|
329
329
|
if (pendingIDs.length === 0) return;
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
330
|
+
const requestCompletionVersion = this.finiteCompletionVersion;
|
|
331
|
+
let fetched;
|
|
332
|
+
if (this.options.source) {
|
|
333
|
+
fetched = await this.options.source.fetchChunks(pendingIDs);
|
|
334
|
+
} else {
|
|
335
|
+
const replicator = this.options.replicatorService.getActiveReplicator();
|
|
336
|
+
if (!replicator) {
|
|
337
|
+
Logger("No active replicator was found to request missing chunks.");
|
|
338
|
+
return;
|
|
339
339
|
}
|
|
340
|
-
|
|
340
|
+
if (!canFetchRemoteChunks(replicator)) {
|
|
341
|
+
Logger("The active replicator does not support fetching individual remote chunks.");
|
|
342
|
+
for (const chunkID of pendingIDs) {
|
|
343
|
+
this.chunkManager.emitEvent(EVENT_MISSING_CHUNK_REMOTE, chunkID);
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
fetched = await replicator.fetchRemoteChunks(pendingIDs, false);
|
|
341
348
|
}
|
|
342
|
-
const requestCompletionVersion = this.finiteCompletionVersion;
|
|
343
|
-
const fetched = await replicator.fetchRemoteChunks(pendingIDs, false);
|
|
344
349
|
pendingIDs = this.getActiveClaimIds(pendingIDs, requestClaims);
|
|
345
350
|
this.touchClaims(pendingIDs, requestClaims);
|
|
346
351
|
if (!fetched) {
|
|
@@ -354,7 +359,9 @@ class ChunkFetcher {
|
|
|
354
359
|
this.logFetchedChunks(pendingIDs, classification);
|
|
355
360
|
const { chunks, missingIds, invalidIds } = classification;
|
|
356
361
|
try {
|
|
357
|
-
if (chunks.length > 0) {
|
|
362
|
+
if (chunks.length > 0 && this.options.source) {
|
|
363
|
+
await this.options.source.storeChunks(chunks);
|
|
364
|
+
} else if (chunks.length > 0) {
|
|
358
365
|
Logger(`Writing fetched chunks (${chunks.length}) to the database...`);
|
|
359
366
|
const result = await this.chunkManager.write(
|
|
360
367
|
chunks,
|
|
@@ -2,6 +2,7 @@ import { type EntryDoc } from "../common/types.js";
|
|
|
2
2
|
import { ContentSplitter } from "../ContentSplitter/ContentSplitters.js";
|
|
3
3
|
import { ChangeManager } from "./ChangeManager.js";
|
|
4
4
|
import { ChunkFetcher } from "./ChunkFetcher.js";
|
|
5
|
+
import type { ChunkFetchSource } from "./chunkFetchSource.js";
|
|
5
6
|
import { ChunkManager } from "./ChunkManager.js";
|
|
6
7
|
import { ConflictManager } from "./ConflictManager.js";
|
|
7
8
|
import { EntryManager } from "./EntryManager/EntryManager.js";
|
|
@@ -15,6 +16,7 @@ export interface LiveSyncManagersOptions<TSettingService extends ISettingService
|
|
|
15
16
|
pathService: IPathService;
|
|
16
17
|
replicatorService: IReplicatorService;
|
|
17
18
|
APIService: IAPIService;
|
|
19
|
+
chunkFetchSource?: ChunkFetchSource;
|
|
18
20
|
}
|
|
19
21
|
export declare class LiveSyncManagers {
|
|
20
22
|
protected _pathService: IPathService;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DocumentID, EntryDoc, EntryLeaf } from "../common/types.js";
|
|
2
|
+
/** IO supplied by a host whose chunk source does not use the active replicator. */
|
|
3
|
+
export interface ChunkFetchSource {
|
|
4
|
+
/** Missing identifiers are omitted; false means that this request could not retrieve chunks. */
|
|
5
|
+
fetchChunks(ids: DocumentID[]): Promise<EntryLeaf[] | false>;
|
|
6
|
+
/** Accept fetched chunks before notifying waiting readers. */
|
|
7
|
+
storeChunks(chunks: EntryLeaf[]): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read through the prepared direct database, including its decryption transforms.
|
|
11
|
+
* The source already owns these documents, so accepting them performs no write.
|
|
12
|
+
* Construct a new source for each database lifetime.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createDirectChunkFetchSource(database: PouchDB.Database<EntryDoc>): ChunkFetchSource;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function createDirectChunkFetchSource(database) {
|
|
2
|
+
return {
|
|
3
|
+
async fetchChunks(ids) {
|
|
4
|
+
const result = await database.allDocs({ keys: ids, include_docs: true });
|
|
5
|
+
return result.rows.flatMap((row) => "doc" in row && row.doc ? [row.doc] : []);
|
|
6
|
+
},
|
|
7
|
+
storeChunks: () => Promise.resolve()
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
createDirectChunkFetchSource
|
|
12
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** More than one listed entry has the requested logical identity. */
|
|
2
|
+
export declare class AmbiguousNormalisedPathError extends Error {
|
|
3
|
+
/** Path whose logical identity was ambiguous. */
|
|
4
|
+
readonly path: string;
|
|
5
|
+
/** Physical candidate paths copied from the listing which exposed the ambiguity. */
|
|
6
|
+
readonly candidates: readonly string[];
|
|
7
|
+
/**
|
|
8
|
+
* Describe an ambiguous request without choosing a physical candidate.
|
|
9
|
+
*
|
|
10
|
+
* @param path - Path whose logical identity matched multiple entries.
|
|
11
|
+
* @param candidates - Matching physical paths, copied into a frozen array.
|
|
12
|
+
*/
|
|
13
|
+
constructor(
|
|
14
|
+
/** Path whose logical identity was ambiguous. */
|
|
15
|
+
path: string, candidates?: readonly string[]);
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class AmbiguousNormalisedPathError extends Error {
|
|
2
|
+
/**
|
|
3
|
+
* Describe an ambiguous request without choosing a physical candidate.
|
|
4
|
+
*
|
|
5
|
+
* @param path - Path whose logical identity matched multiple entries.
|
|
6
|
+
* @param candidates - Matching physical paths, copied into a frozen array.
|
|
7
|
+
*/
|
|
8
|
+
constructor(path, candidates = []) {
|
|
9
|
+
const detail = candidates.length ? ` (${candidates.map((name) => JSON.stringify(name)).join(", ")})` : "";
|
|
10
|
+
super(`Ambiguous path after Unicode normalisation: ${path}${detail}`);
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.name = "AmbiguousNormalisedPathError";
|
|
13
|
+
this.candidates = Object.freeze([...candidates]);
|
|
14
|
+
}
|
|
15
|
+
path;
|
|
16
|
+
/** Physical candidate paths copied from the listing which exposed the ambiguity. */
|
|
17
|
+
candidates;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
AmbiguousNormalisedPathError
|
|
21
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FilePath, ResolvedRename, StorageEntry, StoragePath, StoragePathResolver as ResolverContract, StoragePathResolverOptions } from "./pathTypes.js";
|
|
2
|
+
/** Resolves listed names using one immutable NFC policy and no retained directory cache. */
|
|
3
|
+
export declare class StoragePathResolver implements ResolverContract {
|
|
4
|
+
private readonly readDirectory;
|
|
5
|
+
private readonly policy;
|
|
6
|
+
constructor({ readDirectory, policy }: StoragePathResolverOptions);
|
|
7
|
+
resolveExisting(path: FilePath): Promise<StoragePath | null>;
|
|
8
|
+
resolveForWrite(path: FilePath): Promise<StoragePath>;
|
|
9
|
+
resolveRename(source: FilePath, target: FilePath): Promise<ResolvedRename | null>;
|
|
10
|
+
listResolvedDirectory(path: StoragePath): Promise<readonly StorageEntry[] | null>;
|
|
11
|
+
private resolve;
|
|
12
|
+
private findEntry;
|
|
13
|
+
private readEntries;
|
|
14
|
+
private key;
|
|
15
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getPathNormalizationVersion } from "../common/pathNormalization.js";
|
|
2
|
+
import { AmbiguousNormalisedPathError } from "./AmbiguousNormalisedPathError.js";
|
|
3
|
+
import { getStoragePathComparisonKey } from "./pathComparison.js";
|
|
4
|
+
import { validateStoragePath } from "./storagePath.js";
|
|
5
|
+
function storagePath(path) {
|
|
6
|
+
return validateStoragePath(path);
|
|
7
|
+
}
|
|
8
|
+
function childPath(parent, name) {
|
|
9
|
+
return storagePath(parent === "" ? name : `${parent}/${name}`);
|
|
10
|
+
}
|
|
11
|
+
function segments(path) {
|
|
12
|
+
validateStoragePath(path);
|
|
13
|
+
return path.split("/").filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
class StoragePathResolver {
|
|
16
|
+
readDirectory;
|
|
17
|
+
policy;
|
|
18
|
+
constructor({ readDirectory, policy }) {
|
|
19
|
+
if (getPathNormalizationVersion(policy) !== 1) {
|
|
20
|
+
throw new Error("Storage path resolution requires NFC path identity");
|
|
21
|
+
}
|
|
22
|
+
if (typeof policy.handleFilenameCaseSensitive !== "boolean") {
|
|
23
|
+
throw new TypeError("Filename case handling must be explicit");
|
|
24
|
+
}
|
|
25
|
+
this.readDirectory = readDirectory;
|
|
26
|
+
this.policy = Object.freeze({ ...policy });
|
|
27
|
+
}
|
|
28
|
+
async resolveExisting(path) {
|
|
29
|
+
return await this.resolve(path, false);
|
|
30
|
+
}
|
|
31
|
+
async resolveForWrite(path) {
|
|
32
|
+
const resolved = await this.resolve(path, true);
|
|
33
|
+
if (resolved === null) throw new Error(`Could not resolve storage target: ${path}`);
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
async resolveRename(source, target) {
|
|
37
|
+
validateStoragePath(source, false);
|
|
38
|
+
validateStoragePath(target, false);
|
|
39
|
+
const resolvedSource = await this.resolveExisting(source);
|
|
40
|
+
if (resolvedSource === null) return null;
|
|
41
|
+
const resolvedTarget = await this.resolveForWrite(target);
|
|
42
|
+
if (resolvedTarget !== resolvedSource || target === resolvedSource) {
|
|
43
|
+
return { source: resolvedSource, target: resolvedTarget };
|
|
44
|
+
}
|
|
45
|
+
const requestedParts = segments(target);
|
|
46
|
+
const leaf = requestedParts[requestedParts.length - 1];
|
|
47
|
+
const parent = storagePath(resolvedTarget.slice(0, Math.max(0, resolvedTarget.lastIndexOf("/"))));
|
|
48
|
+
return { source: resolvedSource, target: childPath(parent, leaf) };
|
|
49
|
+
}
|
|
50
|
+
async listResolvedDirectory(path) {
|
|
51
|
+
validateStoragePath(path);
|
|
52
|
+
const entries = await this.readEntries(path);
|
|
53
|
+
if (entries === null) return null;
|
|
54
|
+
const names = /* @__PURE__ */ new Map();
|
|
55
|
+
return entries.map((entry) => {
|
|
56
|
+
const actualPath = childPath(path, entry.name);
|
|
57
|
+
const key = this.key(entry.name);
|
|
58
|
+
const previous = names.get(key);
|
|
59
|
+
if (previous !== void 0) {
|
|
60
|
+
throw new AmbiguousNormalisedPathError(path, [previous, actualPath]);
|
|
61
|
+
}
|
|
62
|
+
names.set(key, actualPath);
|
|
63
|
+
return { path: actualPath, kind: entry.kind };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async resolve(path, allowMissing) {
|
|
67
|
+
const parts = segments(path);
|
|
68
|
+
let parent = storagePath("");
|
|
69
|
+
if (parts.length === 0) {
|
|
70
|
+
return allowMissing || await this.readEntries(parent) !== null ? parent : null;
|
|
71
|
+
}
|
|
72
|
+
for (let index = 0; index < parts.length; index++) {
|
|
73
|
+
const entries = await this.readEntries(parent);
|
|
74
|
+
const match = entries === null ? void 0 : this.findEntry(entries, parts[index], parent, path);
|
|
75
|
+
if (!match) {
|
|
76
|
+
return allowMissing ? childPath(parent, parts.slice(index).join("/")) : null;
|
|
77
|
+
}
|
|
78
|
+
parent = childPath(parent, match.name);
|
|
79
|
+
if (index < parts.length - 1 && match.kind !== "directory") {
|
|
80
|
+
throw new Error(`A storage path parent is not a directory: ${parent}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return parent;
|
|
84
|
+
}
|
|
85
|
+
findEntry(entries, name, parent, requestedPath) {
|
|
86
|
+
const key = this.key(name);
|
|
87
|
+
const matches = entries.filter((entry) => this.key(entry.name) === key);
|
|
88
|
+
if (matches.length > 1) {
|
|
89
|
+
throw new AmbiguousNormalisedPathError(
|
|
90
|
+
requestedPath,
|
|
91
|
+
matches.map((entry) => childPath(parent, entry.name))
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return matches[0];
|
|
95
|
+
}
|
|
96
|
+
async readEntries(path) {
|
|
97
|
+
const entries = await this.readDirectory(path);
|
|
98
|
+
if (entries === null) return null;
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (typeof entry.name !== "string" || entry.name === "" || entry.name === "." || entry.name === ".." || /[\/\\\0]/u.test(entry.name) || entry.kind !== "file" && entry.kind !== "directory") {
|
|
101
|
+
throw new TypeError(`Invalid directory entry below ${JSON.stringify(path)}`);
|
|
102
|
+
}
|
|
103
|
+
childPath(path, entry.name);
|
|
104
|
+
}
|
|
105
|
+
return entries;
|
|
106
|
+
}
|
|
107
|
+
key(name) {
|
|
108
|
+
return getStoragePathComparisonKey(name, this.policy);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
StoragePathResolver
|
|
113
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { UXDataWriteOptions } from "../../common/types.js";
|
|
2
|
+
import type { IStorageAdapter } from "../../serviceModules/adapters/IStorageAdapter.js";
|
|
3
|
+
import type { FilePath, StoragePath } from "../pathTypes.js";
|
|
4
|
+
import { type FileSystemAccessStorageStat } from "./storage.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve NFC-equivalent logical paths against current File System Access directory names.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Each operation snapshots Unicode and case settings. Version `0` delegates to
|
|
10
|
+
* the existing adapter; version `1` resolves names and rejects observed aliases
|
|
11
|
+
* before I/O. Physical spelling and the host adapter's mutation semantics remain
|
|
12
|
+
* unchanged. Resolution does not reserve paths against concurrent changes.
|
|
13
|
+
*/
|
|
14
|
+
export declare class UnicodeResolvingFileSystemAccessStorageAdapter implements IStorageAdapter<FileSystemAccessStorageStat> {
|
|
15
|
+
private readonly getPathNormalizationVersion;
|
|
16
|
+
private readonly isCaseInsensitive;
|
|
17
|
+
private readonly storage;
|
|
18
|
+
/**
|
|
19
|
+
* Bind a host-owned root and callbacks for each operation's comparison settings.
|
|
20
|
+
*
|
|
21
|
+
* @param rootHandle - Host-selected directory handle with the required permissions.
|
|
22
|
+
* @param getPathNormalizationVersion - Supplies `0` or `1`; defaults to Legacy `0`.
|
|
23
|
+
* @param isCaseInsensitive - Whether comparisons ignore case; defaults to false.
|
|
24
|
+
*/
|
|
25
|
+
constructor(rootHandle: FileSystemDirectoryHandle, getPathNormalizationVersion?: () => number, isCaseInsensitive?: () => boolean);
|
|
26
|
+
/** Whether the current settings select NFC path resolution. */
|
|
27
|
+
get isUnicodePathNormalizationEnabled(): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a logical path under one snapshot of the current settings.
|
|
30
|
+
*
|
|
31
|
+
* @param path - Original root-relative file path.
|
|
32
|
+
* @param caseInsensitive - Override this lookup's case rule without changing settings.
|
|
33
|
+
* @param allowMissing - Keep a missing write suffix when true; defaults to true.
|
|
34
|
+
* @returns The physical target, or `null` for an absent path when `allowMissing`
|
|
35
|
+
* is false in NFC mode. Legacy mode returns the validated original path.
|
|
36
|
+
* @throws Propagates NFC ambiguity and reader failures, and rejects invalid input.
|
|
37
|
+
*/
|
|
38
|
+
resolveLogicalPath(path: FilePath, caseInsensitive?: boolean, allowMissing?: boolean): Promise<StoragePath | null>;
|
|
39
|
+
/** Read metadata for a physical path without performing another logical lookup. */
|
|
40
|
+
statExact(path: StoragePath): Promise<FileSystemAccessStorageStat | null>;
|
|
41
|
+
/** List a physical directory, rejecting equivalent child names in NFC mode. */
|
|
42
|
+
listExact(path: StoragePath): Promise<{
|
|
43
|
+
files: string[];
|
|
44
|
+
folders: string[];
|
|
45
|
+
}>;
|
|
46
|
+
/** Check whether the logical path exists under the current Unicode policy. */
|
|
47
|
+
exists(path: string): Promise<boolean>;
|
|
48
|
+
/** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
|
|
49
|
+
trystat(path: string): Promise<FileSystemAccessStorageStat | null>;
|
|
50
|
+
/** Read logical-path metadata, retaining Legacy adapter behaviour in version `0`. */
|
|
51
|
+
stat(path: string): Promise<FileSystemAccessStorageStat | null>;
|
|
52
|
+
/** Create a directory at the resolved write target. */
|
|
53
|
+
mkdir(path: string): Promise<void>;
|
|
54
|
+
/** Remove the entry at its resolved physical path. */
|
|
55
|
+
remove(path: string): Promise<void>;
|
|
56
|
+
/** Read text from the resolved physical file. */
|
|
57
|
+
read(path: string): Promise<string>;
|
|
58
|
+
/** Read bytes from the resolved physical file. */
|
|
59
|
+
readBinary(path: string): Promise<ArrayBuffer>;
|
|
60
|
+
/** Write text at the resolved target while retaining existing physical spelling. */
|
|
61
|
+
write(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
|
|
62
|
+
/** Write bytes at the resolved target while retaining existing physical spelling. */
|
|
63
|
+
writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void>;
|
|
64
|
+
/** Append text at the resolved target using the host adapter semantics. */
|
|
65
|
+
append(path: string, data: string, options?: UXDataWriteOptions): Promise<void>;
|
|
66
|
+
/** List logical directory contents, checking equivalent child names in NFC mode. */
|
|
67
|
+
list(basePath: string): Promise<{
|
|
68
|
+
files: string[];
|
|
69
|
+
folders: string[];
|
|
70
|
+
}>;
|
|
71
|
+
private snapshotPolicy;
|
|
72
|
+
private createResolver;
|
|
73
|
+
private groupStorageEntries;
|
|
74
|
+
}
|