zen-fs-config 0.5.30 → 0.5.32
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/dist/{backend-registry-7OTQSRXY.mjs → backend-registry-7QGFWLM7.mjs} +1 -1
- package/dist/{chunk-XLOBWTYJ.mjs → chunk-UDEOYFWS.mjs} +37 -10
- package/dist/index.d.mts +98 -1
- package/dist/index.d.ts +98 -1
- package/dist/index.js +148 -10
- package/dist/index.mjs +111 -2
- package/dist/zen-fs-config.js +146 -10
- package/package.json +1 -1
|
@@ -184,15 +184,24 @@ var OPTIONAL_BACKEND_GLOBALS = {
|
|
|
184
184
|
};
|
|
185
185
|
async function importOptionalBackend(pkg) {
|
|
186
186
|
const candidates = OPTIONAL_BACKEND_GLOBALS[pkg];
|
|
187
|
+
const fromGlobal = () => {
|
|
188
|
+
if (!candidates) return void 0;
|
|
189
|
+
for (const name of candidates) {
|
|
190
|
+
const g = globalThis[name];
|
|
191
|
+
if (g) return g;
|
|
192
|
+
}
|
|
193
|
+
return void 0;
|
|
194
|
+
};
|
|
187
195
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const g = globalThis[name];
|
|
193
|
-
if (g) return g;
|
|
194
|
-
}
|
|
196
|
+
const mod = await import(pkg);
|
|
197
|
+
if (mod && Object.keys(mod).length === 0) {
|
|
198
|
+
const g = fromGlobal();
|
|
199
|
+
if (g) return g;
|
|
195
200
|
}
|
|
201
|
+
return mod;
|
|
202
|
+
} catch {
|
|
203
|
+
const g = fromGlobal();
|
|
204
|
+
if (g) return g;
|
|
196
205
|
const globalHint = candidates?.length ? `, or load its UMD bundle via <script src=".../${pkg}.js"> (exposes global "${candidates[0]}")` : "";
|
|
197
206
|
throw new Error(
|
|
198
207
|
`Backend package "${pkg}" is not available. In a bundler/import-map environment run: npm install ${pkg}${globalHint}.`
|
|
@@ -200,7 +209,13 @@ async function importOptionalBackend(pkg) {
|
|
|
200
209
|
}
|
|
201
210
|
}
|
|
202
211
|
registerBackend("GitHub", async (options) => {
|
|
203
|
-
const
|
|
212
|
+
const _gh = await importOptionalBackend("zen-fs-github");
|
|
213
|
+
const Github = _gh?.Github ?? _gh?.default;
|
|
214
|
+
if (typeof Github !== "function") {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Backend package "zen-fs-github" loaded, but its Github export is not a constructor (typeof=${typeof Github}). Load its UMD bundle so it exposes the global "ZenFSGitHub".`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
204
219
|
const backend = await wrapZenFSFileSystem({
|
|
205
220
|
backend: Github,
|
|
206
221
|
token: options.token,
|
|
@@ -248,7 +263,13 @@ registerBackend("GitHub", async (options) => {
|
|
|
248
263
|
accountFields: ["token", "owner", "baseUrl"]
|
|
249
264
|
});
|
|
250
265
|
registerBackend("Gitee", async (options) => {
|
|
251
|
-
const
|
|
266
|
+
const _gt = await importOptionalBackend("zen-fs-gitee");
|
|
267
|
+
const Gitee = _gt?.Gitee ?? _gt?.default;
|
|
268
|
+
if (typeof Gitee !== "function") {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Backend package "zen-fs-gitee" loaded, but its Gitee export is not a constructor (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
252
273
|
return wrapZenFSFileSystem({
|
|
253
274
|
backend: Gitee,
|
|
254
275
|
token: options.token,
|
|
@@ -272,7 +293,13 @@ registerBackend("Gitee", async (options) => {
|
|
|
272
293
|
accountFields: ["token", "owner", "baseUrl"]
|
|
273
294
|
});
|
|
274
295
|
registerBackend("RemoteStorage", async (options) => {
|
|
275
|
-
const
|
|
296
|
+
const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
|
|
297
|
+
const RemoteStorageFileSystem = _rs?.RemoteStorageFileSystem ?? _rs?.default ?? (typeof _rs === "function" ? _rs : void 0);
|
|
298
|
+
if (typeof RemoteStorageFileSystem !== "function") {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Backend package "zen-fs-remotestoragejs" loaded, but its RemoteStorageFileSystem export is not a constructor (typeof=${typeof RemoteStorageFileSystem}). Load its UMD bundle via <script src=".../zen-fs-remotestoragejs.global.js"> so it exposes the global "ZenFSRemoteStorage".`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
276
303
|
return new RemoteStorageFileSystem({
|
|
277
304
|
href: options.href,
|
|
278
305
|
token: options.token,
|
package/dist/index.d.mts
CHANGED
|
@@ -4,6 +4,65 @@ import * as node_fs from 'node:fs';
|
|
|
4
4
|
import * as node_path from 'node:path';
|
|
5
5
|
import { SyncMapStore, SyncMapTransaction } from '@zenfs/core';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* zen-fs-config — mtime sidecar cleanup
|
|
9
|
+
*
|
|
10
|
+
* Backends that cannot store a precise mtime natively (RemoteStorage, Gitee,
|
|
11
|
+
* …) persist it in a `.mtime` sidecar next to each file
|
|
12
|
+
* (`data.json` → `.data.json.mtime`). A sidecar is backend-internal metadata:
|
|
13
|
+
* it belongs to the backend that produced it and must never be handed out by
|
|
14
|
+
* `readdir()`/`createSnapshot()`.
|
|
15
|
+
*
|
|
16
|
+
* Older builds did leak them, and once a sidecar lands in the local primary
|
|
17
|
+
* (IndexedDB on browser, Folder on Node) it stays there forever — zen-fs-sync
|
|
18
|
+
* deliberately skips `.mtime` paths on both sides, so nothing ever removes
|
|
19
|
+
* them, and every walk logs:
|
|
20
|
+
*
|
|
21
|
+
* [zen-fs-sync] mtime sidecar leaked from backend "local-idb" …
|
|
22
|
+
*
|
|
23
|
+
* This module walks the local primary and deletes those leaked files. It never
|
|
24
|
+
* touches replica (remote) backends: their sidecars are live metadata, and
|
|
25
|
+
* deleting them would cost network calls and lose mtime precision.
|
|
26
|
+
*/
|
|
27
|
+
/** Minimal async FS surface needed for the purge. */
|
|
28
|
+
interface PurgeableFS {
|
|
29
|
+
readdir(path: string): Promise<string[]>;
|
|
30
|
+
stat(path: string): Promise<{
|
|
31
|
+
mode?: number;
|
|
32
|
+
}>;
|
|
33
|
+
unlink(path: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
interface PurgeMtimeOptions {
|
|
36
|
+
/** Root to scan. Default: `/` (the whole local primary). */
|
|
37
|
+
root?: string;
|
|
38
|
+
/** Report what would be deleted without unlinking anything. */
|
|
39
|
+
dryRun?: boolean;
|
|
40
|
+
}
|
|
41
|
+
interface MtimePurgeResult {
|
|
42
|
+
/** Files visited during the walk (sidecars included). */
|
|
43
|
+
scanned: number;
|
|
44
|
+
/** Sidecars deleted — or, in dryRun, that would be deleted. */
|
|
45
|
+
removed: string[];
|
|
46
|
+
/** Sidecars that could not be deleted (unlink failed). */
|
|
47
|
+
failed: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* True when a file name is an mtime sidecar.
|
|
51
|
+
*
|
|
52
|
+
* Uses the same rule as zen-fs-sync's walker (`path.endsWith('.mtime')`), so
|
|
53
|
+
* the pathological nested form (`.foo.mtime.mtime`) is covered too.
|
|
54
|
+
*/
|
|
55
|
+
declare function isMtimeSidecar(fileName: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Delete every `.mtime` sidecar stored in a local primary backend.
|
|
58
|
+
*
|
|
59
|
+
* Walks the whole tree from `options.root` — including dotfiles and `/.meta/`,
|
|
60
|
+
* because sidecars live next to their data file and are themselves dotfiles.
|
|
61
|
+
* Unreadable entries are skipped; failures on individual files are collected
|
|
62
|
+
* in `failed` instead of aborting the walk.
|
|
63
|
+
*/
|
|
64
|
+
declare function purgeMtimeSidecars(fs: PurgeableFS, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
65
|
+
|
|
7
66
|
/** A single backend in the topology. */
|
|
8
67
|
interface BackendDescriptor {
|
|
9
68
|
/** Unique identifier within this config repo (e.g., "local-idb"). */
|
|
@@ -159,6 +218,17 @@ interface ConfigRepoOptions {
|
|
|
159
218
|
* Default: 1800000 (30 minutes).
|
|
160
219
|
*/
|
|
161
220
|
syncPollIntervalMs?: number;
|
|
221
|
+
/**
|
|
222
|
+
* Delete leaked `.mtime` sidecars from the local primary on startup.
|
|
223
|
+
*
|
|
224
|
+
* Backends that keep a precise mtime out-of-band (RemoteStorage, Gitee…)
|
|
225
|
+
* write a `.mtime` file next to each data file. Older builds copied those
|
|
226
|
+
* backend-internal files into the local primary, where sync ignores them —
|
|
227
|
+
* so they linger forever and warn on every walk.
|
|
228
|
+
*
|
|
229
|
+
* Default: `true`. Pass `false` to skip the (local-only) startup sweep.
|
|
230
|
+
*/
|
|
231
|
+
purgeMtimeSidecars?: boolean;
|
|
162
232
|
}
|
|
163
233
|
/** Type of sync group. */
|
|
164
234
|
type SyncGroupType = 'config-sync' | 'data-sync';
|
|
@@ -340,6 +410,17 @@ interface IConfigRepo {
|
|
|
340
410
|
* Called automatically by createConfigRepo() after setupSync().
|
|
341
411
|
*/
|
|
342
412
|
syncMetaToReplicas(): Promise<void>;
|
|
413
|
+
/**
|
|
414
|
+
* Delete leaked `.mtime` sidecars from the local primary backend.
|
|
415
|
+
*
|
|
416
|
+
* Runs automatically on `createConfigRepo()` unless
|
|
417
|
+
* `options.purgeMtimeSidecars === false`. Call it manually to clean a
|
|
418
|
+
* long-lived store, or with `{ dryRun: true }` to only list them.
|
|
419
|
+
*/
|
|
420
|
+
purgeMtimeSidecars(options?: {
|
|
421
|
+
root?: string;
|
|
422
|
+
dryRun?: boolean;
|
|
423
|
+
}): Promise<MtimePurgeResult>;
|
|
343
424
|
/**
|
|
344
425
|
* Create a data-sync group for this app.
|
|
345
426
|
* Each backend can optionally reference a config-sync backend's account
|
|
@@ -593,6 +674,22 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
593
674
|
private safeExists;
|
|
594
675
|
/** Public wrapper for processTombstones — used by createConfigRepo. */
|
|
595
676
|
processTombstonesPublic(): Promise<void>;
|
|
677
|
+
/**
|
|
678
|
+
* Delete leaked `.mtime` sidecar files from the local primary backend
|
|
679
|
+
* (IndexedDB on browser, Folder on Node).
|
|
680
|
+
*
|
|
681
|
+
* Sidecars are produced by backends that keep a precise mtime out-of-band
|
|
682
|
+
* (RemoteStorage, Gitee…). Once one is copied into the local primary it is
|
|
683
|
+
* never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
|
|
684
|
+
* invisible to sync, and every walk warns about the leak.
|
|
685
|
+
*
|
|
686
|
+
* Only the local primary is scanned — replica sidecars are live metadata of
|
|
687
|
+
* the backend that owns them and must be left alone.
|
|
688
|
+
*
|
|
689
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
690
|
+
* @param options.dryRun List the sidecars without deleting them.
|
|
691
|
+
*/
|
|
692
|
+
purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
596
693
|
/**
|
|
597
694
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
598
695
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -826,4 +923,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
826
923
|
*/
|
|
827
924
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
828
925
|
|
|
829
|
-
export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
|
926
|
+
export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,65 @@ import * as node_fs from 'node:fs';
|
|
|
4
4
|
import * as node_path from 'node:path';
|
|
5
5
|
import { SyncMapStore, SyncMapTransaction } from '@zenfs/core';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* zen-fs-config — mtime sidecar cleanup
|
|
9
|
+
*
|
|
10
|
+
* Backends that cannot store a precise mtime natively (RemoteStorage, Gitee,
|
|
11
|
+
* …) persist it in a `.mtime` sidecar next to each file
|
|
12
|
+
* (`data.json` → `.data.json.mtime`). A sidecar is backend-internal metadata:
|
|
13
|
+
* it belongs to the backend that produced it and must never be handed out by
|
|
14
|
+
* `readdir()`/`createSnapshot()`.
|
|
15
|
+
*
|
|
16
|
+
* Older builds did leak them, and once a sidecar lands in the local primary
|
|
17
|
+
* (IndexedDB on browser, Folder on Node) it stays there forever — zen-fs-sync
|
|
18
|
+
* deliberately skips `.mtime` paths on both sides, so nothing ever removes
|
|
19
|
+
* them, and every walk logs:
|
|
20
|
+
*
|
|
21
|
+
* [zen-fs-sync] mtime sidecar leaked from backend "local-idb" …
|
|
22
|
+
*
|
|
23
|
+
* This module walks the local primary and deletes those leaked files. It never
|
|
24
|
+
* touches replica (remote) backends: their sidecars are live metadata, and
|
|
25
|
+
* deleting them would cost network calls and lose mtime precision.
|
|
26
|
+
*/
|
|
27
|
+
/** Minimal async FS surface needed for the purge. */
|
|
28
|
+
interface PurgeableFS {
|
|
29
|
+
readdir(path: string): Promise<string[]>;
|
|
30
|
+
stat(path: string): Promise<{
|
|
31
|
+
mode?: number;
|
|
32
|
+
}>;
|
|
33
|
+
unlink(path: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
interface PurgeMtimeOptions {
|
|
36
|
+
/** Root to scan. Default: `/` (the whole local primary). */
|
|
37
|
+
root?: string;
|
|
38
|
+
/** Report what would be deleted without unlinking anything. */
|
|
39
|
+
dryRun?: boolean;
|
|
40
|
+
}
|
|
41
|
+
interface MtimePurgeResult {
|
|
42
|
+
/** Files visited during the walk (sidecars included). */
|
|
43
|
+
scanned: number;
|
|
44
|
+
/** Sidecars deleted — or, in dryRun, that would be deleted. */
|
|
45
|
+
removed: string[];
|
|
46
|
+
/** Sidecars that could not be deleted (unlink failed). */
|
|
47
|
+
failed: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* True when a file name is an mtime sidecar.
|
|
51
|
+
*
|
|
52
|
+
* Uses the same rule as zen-fs-sync's walker (`path.endsWith('.mtime')`), so
|
|
53
|
+
* the pathological nested form (`.foo.mtime.mtime`) is covered too.
|
|
54
|
+
*/
|
|
55
|
+
declare function isMtimeSidecar(fileName: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Delete every `.mtime` sidecar stored in a local primary backend.
|
|
58
|
+
*
|
|
59
|
+
* Walks the whole tree from `options.root` — including dotfiles and `/.meta/`,
|
|
60
|
+
* because sidecars live next to their data file and are themselves dotfiles.
|
|
61
|
+
* Unreadable entries are skipped; failures on individual files are collected
|
|
62
|
+
* in `failed` instead of aborting the walk.
|
|
63
|
+
*/
|
|
64
|
+
declare function purgeMtimeSidecars(fs: PurgeableFS, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
65
|
+
|
|
7
66
|
/** A single backend in the topology. */
|
|
8
67
|
interface BackendDescriptor {
|
|
9
68
|
/** Unique identifier within this config repo (e.g., "local-idb"). */
|
|
@@ -159,6 +218,17 @@ interface ConfigRepoOptions {
|
|
|
159
218
|
* Default: 1800000 (30 minutes).
|
|
160
219
|
*/
|
|
161
220
|
syncPollIntervalMs?: number;
|
|
221
|
+
/**
|
|
222
|
+
* Delete leaked `.mtime` sidecars from the local primary on startup.
|
|
223
|
+
*
|
|
224
|
+
* Backends that keep a precise mtime out-of-band (RemoteStorage, Gitee…)
|
|
225
|
+
* write a `.mtime` file next to each data file. Older builds copied those
|
|
226
|
+
* backend-internal files into the local primary, where sync ignores them —
|
|
227
|
+
* so they linger forever and warn on every walk.
|
|
228
|
+
*
|
|
229
|
+
* Default: `true`. Pass `false` to skip the (local-only) startup sweep.
|
|
230
|
+
*/
|
|
231
|
+
purgeMtimeSidecars?: boolean;
|
|
162
232
|
}
|
|
163
233
|
/** Type of sync group. */
|
|
164
234
|
type SyncGroupType = 'config-sync' | 'data-sync';
|
|
@@ -340,6 +410,17 @@ interface IConfigRepo {
|
|
|
340
410
|
* Called automatically by createConfigRepo() after setupSync().
|
|
341
411
|
*/
|
|
342
412
|
syncMetaToReplicas(): Promise<void>;
|
|
413
|
+
/**
|
|
414
|
+
* Delete leaked `.mtime` sidecars from the local primary backend.
|
|
415
|
+
*
|
|
416
|
+
* Runs automatically on `createConfigRepo()` unless
|
|
417
|
+
* `options.purgeMtimeSidecars === false`. Call it manually to clean a
|
|
418
|
+
* long-lived store, or with `{ dryRun: true }` to only list them.
|
|
419
|
+
*/
|
|
420
|
+
purgeMtimeSidecars(options?: {
|
|
421
|
+
root?: string;
|
|
422
|
+
dryRun?: boolean;
|
|
423
|
+
}): Promise<MtimePurgeResult>;
|
|
343
424
|
/**
|
|
344
425
|
* Create a data-sync group for this app.
|
|
345
426
|
* Each backend can optionally reference a config-sync backend's account
|
|
@@ -593,6 +674,22 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
593
674
|
private safeExists;
|
|
594
675
|
/** Public wrapper for processTombstones — used by createConfigRepo. */
|
|
595
676
|
processTombstonesPublic(): Promise<void>;
|
|
677
|
+
/**
|
|
678
|
+
* Delete leaked `.mtime` sidecar files from the local primary backend
|
|
679
|
+
* (IndexedDB on browser, Folder on Node).
|
|
680
|
+
*
|
|
681
|
+
* Sidecars are produced by backends that keep a precise mtime out-of-band
|
|
682
|
+
* (RemoteStorage, Gitee…). Once one is copied into the local primary it is
|
|
683
|
+
* never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
|
|
684
|
+
* invisible to sync, and every walk warns about the leak.
|
|
685
|
+
*
|
|
686
|
+
* Only the local primary is scanned — replica sidecars are live metadata of
|
|
687
|
+
* the backend that owns them and must be left alone.
|
|
688
|
+
*
|
|
689
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
690
|
+
* @param options.dryRun List the sidecars without deleting them.
|
|
691
|
+
*/
|
|
692
|
+
purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
596
693
|
/**
|
|
597
694
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
598
695
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -826,4 +923,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
826
923
|
*/
|
|
827
924
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
828
925
|
|
|
829
|
-
export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
|
926
|
+
export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
package/dist/index.js
CHANGED
|
@@ -187,15 +187,24 @@ function _cacheSet(key, value) {
|
|
|
187
187
|
}
|
|
188
188
|
async function importOptionalBackend(pkg) {
|
|
189
189
|
const candidates = OPTIONAL_BACKEND_GLOBALS[pkg];
|
|
190
|
+
const fromGlobal = () => {
|
|
191
|
+
if (!candidates) return void 0;
|
|
192
|
+
for (const name of candidates) {
|
|
193
|
+
const g = globalThis[name];
|
|
194
|
+
if (g) return g;
|
|
195
|
+
}
|
|
196
|
+
return void 0;
|
|
197
|
+
};
|
|
190
198
|
try {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const g = globalThis[name];
|
|
196
|
-
if (g) return g;
|
|
197
|
-
}
|
|
199
|
+
const mod = await import(pkg);
|
|
200
|
+
if (mod && Object.keys(mod).length === 0) {
|
|
201
|
+
const g = fromGlobal();
|
|
202
|
+
if (g) return g;
|
|
198
203
|
}
|
|
204
|
+
return mod;
|
|
205
|
+
} catch {
|
|
206
|
+
const g = fromGlobal();
|
|
207
|
+
if (g) return g;
|
|
199
208
|
const globalHint = candidates?.length ? `, or load its UMD bundle via <script src=".../${pkg}.js"> (exposes global "${candidates[0]}")` : "";
|
|
200
209
|
throw new Error(
|
|
201
210
|
`Backend package "${pkg}" is not available. In a bundler/import-map environment run: npm install ${pkg}${globalHint}.`
|
|
@@ -249,7 +258,13 @@ var init_backend_registry = __esm({
|
|
|
249
258
|
"zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
|
|
250
259
|
};
|
|
251
260
|
registerBackend("GitHub", async (options) => {
|
|
252
|
-
const
|
|
261
|
+
const _gh = await importOptionalBackend("zen-fs-github");
|
|
262
|
+
const Github = _gh?.Github ?? _gh?.default;
|
|
263
|
+
if (typeof Github !== "function") {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Backend package "zen-fs-github" loaded, but its Github export is not a constructor (typeof=${typeof Github}). Load its UMD bundle so it exposes the global "ZenFSGitHub".`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
253
268
|
const backend = await wrapZenFSFileSystem({
|
|
254
269
|
backend: Github,
|
|
255
270
|
token: options.token,
|
|
@@ -297,7 +312,13 @@ var init_backend_registry = __esm({
|
|
|
297
312
|
accountFields: ["token", "owner", "baseUrl"]
|
|
298
313
|
});
|
|
299
314
|
registerBackend("Gitee", async (options) => {
|
|
300
|
-
const
|
|
315
|
+
const _gt = await importOptionalBackend("zen-fs-gitee");
|
|
316
|
+
const Gitee = _gt?.Gitee ?? _gt?.default;
|
|
317
|
+
if (typeof Gitee !== "function") {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Backend package "zen-fs-gitee" loaded, but its Gitee export is not a constructor (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
301
322
|
return wrapZenFSFileSystem({
|
|
302
323
|
backend: Gitee,
|
|
303
324
|
token: options.token,
|
|
@@ -321,7 +342,13 @@ var init_backend_registry = __esm({
|
|
|
321
342
|
accountFields: ["token", "owner", "baseUrl"]
|
|
322
343
|
});
|
|
323
344
|
registerBackend("RemoteStorage", async (options) => {
|
|
324
|
-
const
|
|
345
|
+
const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
|
|
346
|
+
const RemoteStorageFileSystem = _rs?.RemoteStorageFileSystem ?? _rs?.default ?? (typeof _rs === "function" ? _rs : void 0);
|
|
347
|
+
if (typeof RemoteStorageFileSystem !== "function") {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`Backend package "zen-fs-remotestoragejs" loaded, but its RemoteStorageFileSystem export is not a constructor (typeof=${typeof RemoteStorageFileSystem}). Load its UMD bundle via <script src=".../zen-fs-remotestoragejs.global.js"> so it exposes the global "ZenFSRemoteStorage".`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
325
352
|
return new RemoteStorageFileSystem({
|
|
326
353
|
href: options.href,
|
|
327
354
|
token: options.token,
|
|
@@ -391,10 +418,12 @@ __export(index_exports, {
|
|
|
391
418
|
hasBackend: () => hasBackend,
|
|
392
419
|
incrementVersion: () => incrementVersion,
|
|
393
420
|
isBrowserEnv: () => isBrowserEnv,
|
|
421
|
+
isMtimeSidecar: () => isMtimeSidecar,
|
|
394
422
|
listBackendMetadata: () => listBackendMetadata,
|
|
395
423
|
listBackends: () => listBackends,
|
|
396
424
|
localPrimaryType: () => localPrimaryType,
|
|
397
425
|
mergeAccountFields: () => mergeAccountFields,
|
|
426
|
+
purgeMtimeSidecars: () => purgeMtimeSidecars,
|
|
398
427
|
readVersion: () => readVersion,
|
|
399
428
|
registerBackend: () => registerBackend,
|
|
400
429
|
registerFolderBackend: () => registerFolderBackend,
|
|
@@ -1011,6 +1040,60 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
|
1011
1040
|
}
|
|
1012
1041
|
}
|
|
1013
1042
|
|
|
1043
|
+
// src/mtime-cleanup.ts
|
|
1044
|
+
function isMtimeSidecar(fileName) {
|
|
1045
|
+
return fileName.endsWith(".mtime");
|
|
1046
|
+
}
|
|
1047
|
+
var S_IFDIR = 16384;
|
|
1048
|
+
function isDirectory(stat) {
|
|
1049
|
+
return typeof stat.mode === "number" && (stat.mode & S_IFDIR) === S_IFDIR;
|
|
1050
|
+
}
|
|
1051
|
+
function joinPath(dir, entry) {
|
|
1052
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
1053
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
1054
|
+
}
|
|
1055
|
+
async function purgeMtimeSidecars(fs, options = {}) {
|
|
1056
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
1057
|
+
const dryRun = options.dryRun ?? false;
|
|
1058
|
+
const result = { scanned: 0, removed: [], failed: [] };
|
|
1059
|
+
const stack = [root];
|
|
1060
|
+
while (stack.length > 0) {
|
|
1061
|
+
const dir = stack.pop();
|
|
1062
|
+
let entries;
|
|
1063
|
+
try {
|
|
1064
|
+
entries = await fs.readdir(dir);
|
|
1065
|
+
} catch {
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
for (const entry of entries) {
|
|
1069
|
+
const fullPath = joinPath(dir, entry);
|
|
1070
|
+
let stat;
|
|
1071
|
+
try {
|
|
1072
|
+
stat = await fs.stat(fullPath);
|
|
1073
|
+
} catch {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (isDirectory(stat)) {
|
|
1077
|
+
stack.push(fullPath);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
result.scanned++;
|
|
1081
|
+
if (!isMtimeSidecar(entry)) continue;
|
|
1082
|
+
if (dryRun) {
|
|
1083
|
+
result.removed.push(fullPath);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
await fs.unlink(fullPath);
|
|
1088
|
+
result.removed.push(fullPath);
|
|
1089
|
+
} catch {
|
|
1090
|
+
result.failed.push(fullPath);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1014
1097
|
// src/config-repo.ts
|
|
1015
1098
|
var log2 = (0, import_localstorage_logger2.createLogger)("zen-fs-config:config-repo");
|
|
1016
1099
|
var META_DIR = "/.meta";
|
|
@@ -1464,6 +1547,35 @@ var ConfigRepo = class {
|
|
|
1464
1547
|
async processTombstonesPublic() {
|
|
1465
1548
|
await this.processTombstones();
|
|
1466
1549
|
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Delete leaked `.mtime` sidecar files from the local primary backend
|
|
1552
|
+
* (IndexedDB on browser, Folder on Node).
|
|
1553
|
+
*
|
|
1554
|
+
* Sidecars are produced by backends that keep a precise mtime out-of-band
|
|
1555
|
+
* (RemoteStorage, Gitee…). Once one is copied into the local primary it is
|
|
1556
|
+
* never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
|
|
1557
|
+
* invisible to sync, and every walk warns about the leak.
|
|
1558
|
+
*
|
|
1559
|
+
* Only the local primary is scanned — replica sidecars are live metadata of
|
|
1560
|
+
* the backend that owns them and must be left alone.
|
|
1561
|
+
*
|
|
1562
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
1563
|
+
* @param options.dryRun List the sidecars without deleting them.
|
|
1564
|
+
*/
|
|
1565
|
+
async purgeMtimeSidecars(options) {
|
|
1566
|
+
this.assertNotDisposed();
|
|
1567
|
+
const result = await purgeMtimeSidecars(this.cachedFS, options);
|
|
1568
|
+
if (result.removed.length > 0) {
|
|
1569
|
+
log2.log(
|
|
1570
|
+
`[ConfigRepo] purged ${result.removed.length} .mtime sidecar(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
|
|
1571
|
+
result.removed
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
if (result.failed.length > 0) {
|
|
1575
|
+
log2.warn(`[ConfigRepo] failed to purge ${result.failed.length} .mtime sidecar(s):`, result.failed);
|
|
1576
|
+
}
|
|
1577
|
+
return result;
|
|
1578
|
+
}
|
|
1467
1579
|
/**
|
|
1468
1580
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
1469
1581
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -2385,6 +2497,17 @@ var AppDataGroupImpl = class {
|
|
|
2385
2497
|
} catch {
|
|
2386
2498
|
throw new Error(`Failed to create local primary for data group "${this.groupId}"`);
|
|
2387
2499
|
}
|
|
2500
|
+
try {
|
|
2501
|
+
const purged = await purgeMtimeSidecars(this.localFS);
|
|
2502
|
+
if (purged.removed.length > 0) {
|
|
2503
|
+
log2.warn(
|
|
2504
|
+
`[AppDataGroup:${this.groupId}] purged ${purged.removed.length} leaked .mtime sidecar(s):`,
|
|
2505
|
+
purged.removed
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
} catch (err) {
|
|
2509
|
+
log2.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err?.message ?? err);
|
|
2510
|
+
}
|
|
2388
2511
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
2389
2512
|
this.fs = createChrootFS(this.localFS, "/");
|
|
2390
2513
|
const { kept, removed } = dedupeAppDataBackends(this._backends);
|
|
@@ -2511,6 +2634,19 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
2511
2634
|
log2.log(`[createConfigRepo] Creating local primary (type: ${localPrimary.type})...`);
|
|
2512
2635
|
const primaryInstance = await createBackend(localPrimary);
|
|
2513
2636
|
const cachedFS = primaryInstance;
|
|
2637
|
+
if (options.purgeMtimeSidecars !== false) {
|
|
2638
|
+
try {
|
|
2639
|
+
const purged = await purgeMtimeSidecars(cachedFS);
|
|
2640
|
+
if (purged.removed.length > 0) {
|
|
2641
|
+
log2.warn(
|
|
2642
|
+
`[createConfigRepo] purged ${purged.removed.length} leaked .mtime sidecar(s) from local primary:`,
|
|
2643
|
+
purged.removed
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
} catch (err) {
|
|
2647
|
+
log2.warn(`[createConfigRepo] .mtime purge failed:`, err?.message ?? err);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2514
2650
|
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
2515
2651
|
try {
|
|
2516
2652
|
await primaryInstance.mkdir(META_DIR);
|
|
@@ -2745,10 +2881,12 @@ init_backend_registry();
|
|
|
2745
2881
|
hasBackend,
|
|
2746
2882
|
incrementVersion,
|
|
2747
2883
|
isBrowserEnv,
|
|
2884
|
+
isMtimeSidecar,
|
|
2748
2885
|
listBackendMetadata,
|
|
2749
2886
|
listBackends,
|
|
2750
2887
|
localPrimaryType,
|
|
2751
2888
|
mergeAccountFields,
|
|
2889
|
+
purgeMtimeSidecars,
|
|
2752
2890
|
readVersion,
|
|
2753
2891
|
registerBackend,
|
|
2754
2892
|
registerFolderBackend,
|
package/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
registerBackend,
|
|
10
10
|
unregisterBackend,
|
|
11
11
|
wrapZenFSFileSystem
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-UDEOYFWS.mjs";
|
|
13
13
|
|
|
14
14
|
// src/config-repo.ts
|
|
15
15
|
import {
|
|
@@ -351,7 +351,7 @@ function registerFolderBackend() {
|
|
|
351
351
|
const pathMod = await import("path");
|
|
352
352
|
const dir = options.path;
|
|
353
353
|
const store = new FolderStore({ path: dir, label: options.label, fs, pathMod });
|
|
354
|
-
return (await import("./backend-registry-
|
|
354
|
+
return (await import("./backend-registry-7QGFWLM7.mjs")).wrapZenFSFileSystem({ backend: FolderBackend, path: dir, label: options.label });
|
|
355
355
|
},
|
|
356
356
|
{
|
|
357
357
|
type: "Folder",
|
|
@@ -613,6 +613,60 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
615
|
|
|
616
|
+
// src/mtime-cleanup.ts
|
|
617
|
+
function isMtimeSidecar(fileName) {
|
|
618
|
+
return fileName.endsWith(".mtime");
|
|
619
|
+
}
|
|
620
|
+
var S_IFDIR = 16384;
|
|
621
|
+
function isDirectory(stat) {
|
|
622
|
+
return typeof stat.mode === "number" && (stat.mode & S_IFDIR) === S_IFDIR;
|
|
623
|
+
}
|
|
624
|
+
function joinPath(dir, entry) {
|
|
625
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
626
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
627
|
+
}
|
|
628
|
+
async function purgeMtimeSidecars(fs, options = {}) {
|
|
629
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
630
|
+
const dryRun = options.dryRun ?? false;
|
|
631
|
+
const result = { scanned: 0, removed: [], failed: [] };
|
|
632
|
+
const stack = [root];
|
|
633
|
+
while (stack.length > 0) {
|
|
634
|
+
const dir = stack.pop();
|
|
635
|
+
let entries;
|
|
636
|
+
try {
|
|
637
|
+
entries = await fs.readdir(dir);
|
|
638
|
+
} catch {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
for (const entry of entries) {
|
|
642
|
+
const fullPath = joinPath(dir, entry);
|
|
643
|
+
let stat;
|
|
644
|
+
try {
|
|
645
|
+
stat = await fs.stat(fullPath);
|
|
646
|
+
} catch {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (isDirectory(stat)) {
|
|
650
|
+
stack.push(fullPath);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
result.scanned++;
|
|
654
|
+
if (!isMtimeSidecar(entry)) continue;
|
|
655
|
+
if (dryRun) {
|
|
656
|
+
result.removed.push(fullPath);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
await fs.unlink(fullPath);
|
|
661
|
+
result.removed.push(fullPath);
|
|
662
|
+
} catch {
|
|
663
|
+
result.failed.push(fullPath);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return result;
|
|
668
|
+
}
|
|
669
|
+
|
|
616
670
|
// src/config-repo.ts
|
|
617
671
|
var log2 = createLogger2("zen-fs-config:config-repo");
|
|
618
672
|
var META_DIR = "/.meta";
|
|
@@ -1066,6 +1120,35 @@ var ConfigRepo = class {
|
|
|
1066
1120
|
async processTombstonesPublic() {
|
|
1067
1121
|
await this.processTombstones();
|
|
1068
1122
|
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Delete leaked `.mtime` sidecar files from the local primary backend
|
|
1125
|
+
* (IndexedDB on browser, Folder on Node).
|
|
1126
|
+
*
|
|
1127
|
+
* Sidecars are produced by backends that keep a precise mtime out-of-band
|
|
1128
|
+
* (RemoteStorage, Gitee…). Once one is copied into the local primary it is
|
|
1129
|
+
* never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
|
|
1130
|
+
* invisible to sync, and every walk warns about the leak.
|
|
1131
|
+
*
|
|
1132
|
+
* Only the local primary is scanned — replica sidecars are live metadata of
|
|
1133
|
+
* the backend that owns them and must be left alone.
|
|
1134
|
+
*
|
|
1135
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
1136
|
+
* @param options.dryRun List the sidecars without deleting them.
|
|
1137
|
+
*/
|
|
1138
|
+
async purgeMtimeSidecars(options) {
|
|
1139
|
+
this.assertNotDisposed();
|
|
1140
|
+
const result = await purgeMtimeSidecars(this.cachedFS, options);
|
|
1141
|
+
if (result.removed.length > 0) {
|
|
1142
|
+
log2.log(
|
|
1143
|
+
`[ConfigRepo] purged ${result.removed.length} .mtime sidecar(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
|
|
1144
|
+
result.removed
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
if (result.failed.length > 0) {
|
|
1148
|
+
log2.warn(`[ConfigRepo] failed to purge ${result.failed.length} .mtime sidecar(s):`, result.failed);
|
|
1149
|
+
}
|
|
1150
|
+
return result;
|
|
1151
|
+
}
|
|
1069
1152
|
/**
|
|
1070
1153
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
1071
1154
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -1987,6 +2070,17 @@ var AppDataGroupImpl = class {
|
|
|
1987
2070
|
} catch {
|
|
1988
2071
|
throw new Error(`Failed to create local primary for data group "${this.groupId}"`);
|
|
1989
2072
|
}
|
|
2073
|
+
try {
|
|
2074
|
+
const purged = await purgeMtimeSidecars(this.localFS);
|
|
2075
|
+
if (purged.removed.length > 0) {
|
|
2076
|
+
log2.warn(
|
|
2077
|
+
`[AppDataGroup:${this.groupId}] purged ${purged.removed.length} leaked .mtime sidecar(s):`,
|
|
2078
|
+
purged.removed
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
} catch (err) {
|
|
2082
|
+
log2.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err?.message ?? err);
|
|
2083
|
+
}
|
|
1990
2084
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
1991
2085
|
this.fs = createChrootFS(this.localFS, "/");
|
|
1992
2086
|
const { kept, removed } = dedupeAppDataBackends(this._backends);
|
|
@@ -2113,6 +2207,19 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
2113
2207
|
log2.log(`[createConfigRepo] Creating local primary (type: ${localPrimary.type})...`);
|
|
2114
2208
|
const primaryInstance = await createBackend(localPrimary);
|
|
2115
2209
|
const cachedFS = primaryInstance;
|
|
2210
|
+
if (options.purgeMtimeSidecars !== false) {
|
|
2211
|
+
try {
|
|
2212
|
+
const purged = await purgeMtimeSidecars(cachedFS);
|
|
2213
|
+
if (purged.removed.length > 0) {
|
|
2214
|
+
log2.warn(
|
|
2215
|
+
`[createConfigRepo] purged ${purged.removed.length} leaked .mtime sidecar(s) from local primary:`,
|
|
2216
|
+
purged.removed
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
log2.warn(`[createConfigRepo] .mtime purge failed:`, err?.message ?? err);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2116
2223
|
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
2117
2224
|
try {
|
|
2118
2225
|
await primaryInstance.mkdir(META_DIR);
|
|
@@ -2342,10 +2449,12 @@ export {
|
|
|
2342
2449
|
hasBackend,
|
|
2343
2450
|
incrementVersion,
|
|
2344
2451
|
isBrowserEnv,
|
|
2452
|
+
isMtimeSidecar,
|
|
2345
2453
|
listBackendMetadata,
|
|
2346
2454
|
listBackends,
|
|
2347
2455
|
localPrimaryType,
|
|
2348
2456
|
mergeAccountFields,
|
|
2457
|
+
purgeMtimeSidecars,
|
|
2349
2458
|
readVersion,
|
|
2350
2459
|
registerBackend,
|
|
2351
2460
|
registerFolderBackend,
|
package/dist/zen-fs-config.js
CHANGED
|
@@ -24500,15 +24500,24 @@ var ZenFSConfig = (() => {
|
|
|
24500
24500
|
}
|
|
24501
24501
|
async function importOptionalBackend(pkg) {
|
|
24502
24502
|
const candidates = OPTIONAL_BACKEND_GLOBALS[pkg];
|
|
24503
|
+
const fromGlobal = () => {
|
|
24504
|
+
if (!candidates) return void 0;
|
|
24505
|
+
for (const name of candidates) {
|
|
24506
|
+
const g = globalThis[name];
|
|
24507
|
+
if (g) return g;
|
|
24508
|
+
}
|
|
24509
|
+
return void 0;
|
|
24510
|
+
};
|
|
24503
24511
|
try {
|
|
24504
|
-
|
|
24505
|
-
|
|
24506
|
-
|
|
24507
|
-
|
|
24508
|
-
const g = globalThis[name];
|
|
24509
|
-
if (g) return g;
|
|
24510
|
-
}
|
|
24512
|
+
const mod = await import(pkg);
|
|
24513
|
+
if (mod && Object.keys(mod).length === 0) {
|
|
24514
|
+
const g = fromGlobal();
|
|
24515
|
+
if (g) return g;
|
|
24511
24516
|
}
|
|
24517
|
+
return mod;
|
|
24518
|
+
} catch {
|
|
24519
|
+
const g = fromGlobal();
|
|
24520
|
+
if (g) return g;
|
|
24512
24521
|
const globalHint = candidates?.length ? `, or load its UMD bundle via <script src=".../${pkg}.js"> (exposes global "${candidates[0]}")` : "";
|
|
24513
24522
|
throw new Error(
|
|
24514
24523
|
`Backend package "${pkg}" is not available. In a bundler/import-map environment run: npm install ${pkg}${globalHint}.`
|
|
@@ -24562,7 +24571,13 @@ var ZenFSConfig = (() => {
|
|
|
24562
24571
|
"zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
|
|
24563
24572
|
};
|
|
24564
24573
|
registerBackend("GitHub", async (options) => {
|
|
24565
|
-
const
|
|
24574
|
+
const _gh = await importOptionalBackend("zen-fs-github");
|
|
24575
|
+
const Github = _gh?.Github ?? _gh?.default;
|
|
24576
|
+
if (typeof Github !== "function") {
|
|
24577
|
+
throw new Error(
|
|
24578
|
+
`Backend package "zen-fs-github" loaded, but its Github export is not a constructor (typeof=${typeof Github}). Load its UMD bundle so it exposes the global "ZenFSGitHub".`
|
|
24579
|
+
);
|
|
24580
|
+
}
|
|
24566
24581
|
const backend = await wrapZenFSFileSystem({
|
|
24567
24582
|
backend: Github,
|
|
24568
24583
|
token: options.token,
|
|
@@ -24610,7 +24625,13 @@ var ZenFSConfig = (() => {
|
|
|
24610
24625
|
accountFields: ["token", "owner", "baseUrl"]
|
|
24611
24626
|
});
|
|
24612
24627
|
registerBackend("Gitee", async (options) => {
|
|
24613
|
-
const
|
|
24628
|
+
const _gt = await importOptionalBackend("zen-fs-gitee");
|
|
24629
|
+
const Gitee = _gt?.Gitee ?? _gt?.default;
|
|
24630
|
+
if (typeof Gitee !== "function") {
|
|
24631
|
+
throw new Error(
|
|
24632
|
+
`Backend package "zen-fs-gitee" loaded, but its Gitee export is not a constructor (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
|
|
24633
|
+
);
|
|
24634
|
+
}
|
|
24614
24635
|
return wrapZenFSFileSystem({
|
|
24615
24636
|
backend: Gitee,
|
|
24616
24637
|
token: options.token,
|
|
@@ -24634,7 +24655,13 @@ var ZenFSConfig = (() => {
|
|
|
24634
24655
|
accountFields: ["token", "owner", "baseUrl"]
|
|
24635
24656
|
});
|
|
24636
24657
|
registerBackend("RemoteStorage", async (options) => {
|
|
24637
|
-
const
|
|
24658
|
+
const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
|
|
24659
|
+
const RemoteStorageFileSystem = _rs?.RemoteStorageFileSystem ?? _rs?.default ?? (typeof _rs === "function" ? _rs : void 0);
|
|
24660
|
+
if (typeof RemoteStorageFileSystem !== "function") {
|
|
24661
|
+
throw new Error(
|
|
24662
|
+
`Backend package "zen-fs-remotestoragejs" loaded, but its RemoteStorageFileSystem export is not a constructor (typeof=${typeof RemoteStorageFileSystem}). Load its UMD bundle via <script src=".../zen-fs-remotestoragejs.global.js"> so it exposes the global "ZenFSRemoteStorage".`
|
|
24663
|
+
);
|
|
24664
|
+
}
|
|
24638
24665
|
return new RemoteStorageFileSystem({
|
|
24639
24666
|
href: options.href,
|
|
24640
24667
|
token: options.token,
|
|
@@ -25084,10 +25111,12 @@ var ZenFSConfig = (() => {
|
|
|
25084
25111
|
hasBackend: () => hasBackend,
|
|
25085
25112
|
incrementVersion: () => incrementVersion,
|
|
25086
25113
|
isBrowserEnv: () => isBrowserEnv,
|
|
25114
|
+
isMtimeSidecar: () => isMtimeSidecar,
|
|
25087
25115
|
listBackendMetadata: () => listBackendMetadata,
|
|
25088
25116
|
listBackends: () => listBackends,
|
|
25089
25117
|
localPrimaryType: () => localPrimaryType,
|
|
25090
25118
|
mergeAccountFields: () => mergeAccountFields,
|
|
25119
|
+
purgeMtimeSidecars: () => purgeMtimeSidecars,
|
|
25091
25120
|
readVersion: () => readVersion,
|
|
25092
25121
|
registerBackend: () => registerBackend,
|
|
25093
25122
|
registerFolderBackend: () => registerFolderBackend,
|
|
@@ -26888,6 +26917,60 @@ var ZenFSConfig = (() => {
|
|
|
26888
26917
|
}
|
|
26889
26918
|
}
|
|
26890
26919
|
|
|
26920
|
+
// src/mtime-cleanup.ts
|
|
26921
|
+
function isMtimeSidecar(fileName) {
|
|
26922
|
+
return fileName.endsWith(".mtime");
|
|
26923
|
+
}
|
|
26924
|
+
var S_IFDIR2 = 16384;
|
|
26925
|
+
function isDirectory3(stat5) {
|
|
26926
|
+
return typeof stat5.mode === "number" && (stat5.mode & S_IFDIR2) === S_IFDIR2;
|
|
26927
|
+
}
|
|
26928
|
+
function joinPath(dir, entry) {
|
|
26929
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
26930
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
26931
|
+
}
|
|
26932
|
+
async function purgeMtimeSidecars(fs, options = {}) {
|
|
26933
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
26934
|
+
const dryRun = options.dryRun ?? false;
|
|
26935
|
+
const result = { scanned: 0, removed: [], failed: [] };
|
|
26936
|
+
const stack = [root];
|
|
26937
|
+
while (stack.length > 0) {
|
|
26938
|
+
const dir = stack.pop();
|
|
26939
|
+
let entries2;
|
|
26940
|
+
try {
|
|
26941
|
+
entries2 = await fs.readdir(dir);
|
|
26942
|
+
} catch {
|
|
26943
|
+
continue;
|
|
26944
|
+
}
|
|
26945
|
+
for (const entry of entries2) {
|
|
26946
|
+
const fullPath = joinPath(dir, entry);
|
|
26947
|
+
let stat5;
|
|
26948
|
+
try {
|
|
26949
|
+
stat5 = await fs.stat(fullPath);
|
|
26950
|
+
} catch {
|
|
26951
|
+
continue;
|
|
26952
|
+
}
|
|
26953
|
+
if (isDirectory3(stat5)) {
|
|
26954
|
+
stack.push(fullPath);
|
|
26955
|
+
continue;
|
|
26956
|
+
}
|
|
26957
|
+
result.scanned++;
|
|
26958
|
+
if (!isMtimeSidecar(entry)) continue;
|
|
26959
|
+
if (dryRun) {
|
|
26960
|
+
result.removed.push(fullPath);
|
|
26961
|
+
continue;
|
|
26962
|
+
}
|
|
26963
|
+
try {
|
|
26964
|
+
await fs.unlink(fullPath);
|
|
26965
|
+
result.removed.push(fullPath);
|
|
26966
|
+
} catch {
|
|
26967
|
+
result.failed.push(fullPath);
|
|
26968
|
+
}
|
|
26969
|
+
}
|
|
26970
|
+
}
|
|
26971
|
+
return result;
|
|
26972
|
+
}
|
|
26973
|
+
|
|
26891
26974
|
// src/config-repo.ts
|
|
26892
26975
|
var log6 = createLogger2("zen-fs-config:config-repo");
|
|
26893
26976
|
var META_DIR = "/.meta";
|
|
@@ -27341,6 +27424,35 @@ var ZenFSConfig = (() => {
|
|
|
27341
27424
|
async processTombstonesPublic() {
|
|
27342
27425
|
await this.processTombstones();
|
|
27343
27426
|
}
|
|
27427
|
+
/**
|
|
27428
|
+
* Delete leaked `.mtime` sidecar files from the local primary backend
|
|
27429
|
+
* (IndexedDB on browser, Folder on Node).
|
|
27430
|
+
*
|
|
27431
|
+
* Sidecars are produced by backends that keep a precise mtime out-of-band
|
|
27432
|
+
* (RemoteStorage, Gitee…). Once one is copied into the local primary it is
|
|
27433
|
+
* never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
|
|
27434
|
+
* invisible to sync, and every walk warns about the leak.
|
|
27435
|
+
*
|
|
27436
|
+
* Only the local primary is scanned — replica sidecars are live metadata of
|
|
27437
|
+
* the backend that owns them and must be left alone.
|
|
27438
|
+
*
|
|
27439
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
27440
|
+
* @param options.dryRun List the sidecars without deleting them.
|
|
27441
|
+
*/
|
|
27442
|
+
async purgeMtimeSidecars(options) {
|
|
27443
|
+
this.assertNotDisposed();
|
|
27444
|
+
const result = await purgeMtimeSidecars(this.cachedFS, options);
|
|
27445
|
+
if (result.removed.length > 0) {
|
|
27446
|
+
log6.log(
|
|
27447
|
+
`[ConfigRepo] purged ${result.removed.length} .mtime sidecar(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
|
|
27448
|
+
result.removed
|
|
27449
|
+
);
|
|
27450
|
+
}
|
|
27451
|
+
if (result.failed.length > 0) {
|
|
27452
|
+
log6.warn(`[ConfigRepo] failed to purge ${result.failed.length} .mtime sidecar(s):`, result.failed);
|
|
27453
|
+
}
|
|
27454
|
+
return result;
|
|
27455
|
+
}
|
|
27344
27456
|
/**
|
|
27345
27457
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
27346
27458
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -28262,6 +28374,17 @@ var ZenFSConfig = (() => {
|
|
|
28262
28374
|
} catch {
|
|
28263
28375
|
throw new Error(`Failed to create local primary for data group "${this.groupId}"`);
|
|
28264
28376
|
}
|
|
28377
|
+
try {
|
|
28378
|
+
const purged = await purgeMtimeSidecars(this.localFS);
|
|
28379
|
+
if (purged.removed.length > 0) {
|
|
28380
|
+
log6.warn(
|
|
28381
|
+
`[AppDataGroup:${this.groupId}] purged ${purged.removed.length} leaked .mtime sidecar(s):`,
|
|
28382
|
+
purged.removed
|
|
28383
|
+
);
|
|
28384
|
+
}
|
|
28385
|
+
} catch (err2) {
|
|
28386
|
+
log6.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err2?.message ?? err2);
|
|
28387
|
+
}
|
|
28265
28388
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
28266
28389
|
this.fs = createChrootFS(this.localFS, "/");
|
|
28267
28390
|
const { kept, removed } = dedupeAppDataBackends(this._backends);
|
|
@@ -28388,6 +28511,19 @@ var ZenFSConfig = (() => {
|
|
|
28388
28511
|
log6.log(`[createConfigRepo] Creating local primary (type: ${localPrimary.type})...`);
|
|
28389
28512
|
const primaryInstance = await createBackend(localPrimary);
|
|
28390
28513
|
const cachedFS = primaryInstance;
|
|
28514
|
+
if (options.purgeMtimeSidecars !== false) {
|
|
28515
|
+
try {
|
|
28516
|
+
const purged = await purgeMtimeSidecars(cachedFS);
|
|
28517
|
+
if (purged.removed.length > 0) {
|
|
28518
|
+
log6.warn(
|
|
28519
|
+
`[createConfigRepo] purged ${purged.removed.length} leaked .mtime sidecar(s) from local primary:`,
|
|
28520
|
+
purged.removed
|
|
28521
|
+
);
|
|
28522
|
+
}
|
|
28523
|
+
} catch (err2) {
|
|
28524
|
+
log6.warn(`[createConfigRepo] .mtime purge failed:`, err2?.message ?? err2);
|
|
28525
|
+
}
|
|
28526
|
+
}
|
|
28391
28527
|
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
28392
28528
|
try {
|
|
28393
28529
|
await primaryInstance.mkdir(META_DIR);
|