zen-fs-config 0.5.33 → 0.5.36
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/{cache-wrapper-XQCPZJCD.mjs → cache-wrapper-S5HRLRQX.mjs} +11 -0
- package/dist/index.d.mts +106 -3
- package/dist/index.d.ts +106 -3
- package/dist/index.js +134 -5
- package/dist/index.mjs +122 -6
- package/dist/zen-fs-config.js +189 -6
- package/package.json +2 -2
|
@@ -9,12 +9,23 @@ function wrapWithCache(backend, backendId, options) {
|
|
|
9
9
|
} else {
|
|
10
10
|
store = new MemoryCacheStore();
|
|
11
11
|
}
|
|
12
|
+
try {
|
|
13
|
+
const purge = store.purgeKeepFiles?.();
|
|
14
|
+
if (purge && typeof purge.catch === "function") {
|
|
15
|
+
purge.catch(() => {
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
12
20
|
const wrapped = new CachedFileSystem(backend, store, {
|
|
13
21
|
ttlMs: options.ttlMs ?? 0
|
|
14
22
|
});
|
|
15
23
|
if (typeof backend.shouldSync === "function") {
|
|
16
24
|
wrapped.shouldSync = (...args) => backend.shouldSync(...args);
|
|
17
25
|
}
|
|
26
|
+
if (typeof backend.writeFileWithMtime === "function") {
|
|
27
|
+
wrapped.writeFileWithMtime = (path, data, mtimeMs) => backend.writeFileWithMtime(path, data, mtimeMs);
|
|
28
|
+
}
|
|
18
29
|
return wrapped;
|
|
19
30
|
}
|
|
20
31
|
export {
|
package/dist/index.d.mts
CHANGED
|
@@ -25,7 +25,7 @@ import { SyncMapStore, SyncMapTransaction } from '@zenfs/core';
|
|
|
25
25
|
* deleting them would cost network calls and lose mtime precision.
|
|
26
26
|
*/
|
|
27
27
|
/** Minimal async FS surface needed for the purge. */
|
|
28
|
-
interface PurgeableFS {
|
|
28
|
+
interface PurgeableFS$1 {
|
|
29
29
|
readdir(path: string): Promise<string[]>;
|
|
30
30
|
stat(path: string): Promise<{
|
|
31
31
|
mode?: number;
|
|
@@ -61,7 +61,74 @@ declare function isMtimeSidecar(fileName: string): boolean;
|
|
|
61
61
|
* Unreadable entries are skipped; failures on individual files are collected
|
|
62
62
|
* in `failed` instead of aborting the walk.
|
|
63
63
|
*/
|
|
64
|
-
declare function purgeMtimeSidecars(fs: PurgeableFS, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
64
|
+
declare function purgeMtimeSidecars(fs: PurgeableFS$1, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* zen-fs-config — `.keep` placeholder cleanup (local primary only)
|
|
68
|
+
*
|
|
69
|
+
* Backends that cannot store empty directories (Git, RemoteStorage…) keep a
|
|
70
|
+
* directory alive with an internal `.keep` placeholder — exactly like the
|
|
71
|
+
* `.mtime` sidecar, this is backend-internal metadata that MUST be hidden from
|
|
72
|
+
* callers (see zen-fs-sync/docs/SyncableFS.md §1/§2).
|
|
73
|
+
*
|
|
74
|
+
* Older builds leaked `.keep` files into the local primary (IndexedDB on
|
|
75
|
+
* browser, Folder on Node), where they linger forever: the sync engine now
|
|
76
|
+
* skips `.keep` paths on both sides, so nothing ever removes them, and every
|
|
77
|
+
* walk re-creates confusion about which directories are "real".
|
|
78
|
+
*
|
|
79
|
+
* This module walks the local primary and deletes those leaked `.keep` files.
|
|
80
|
+
* It never touches replica (remote) backends: their placeholders are live
|
|
81
|
+
* metadata that keep real (empty) directories alive, and deleting them would
|
|
82
|
+
* lose directory structure.
|
|
83
|
+
*
|
|
84
|
+
* The intentional `.keep` under `/.meta/` (used to keep the backends directory
|
|
85
|
+
* alive) is protected and never deleted — see `protectedDirs`.
|
|
86
|
+
*/
|
|
87
|
+
/** Minimal async FS surface needed for the purge. */
|
|
88
|
+
interface PurgeableFS {
|
|
89
|
+
readdir(path: string): Promise<string[]>;
|
|
90
|
+
stat(path: string): Promise<{
|
|
91
|
+
mode?: number;
|
|
92
|
+
}>;
|
|
93
|
+
unlink(path: string): Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
interface PurgeKeepOptions {
|
|
96
|
+
/** Root to scan. Default: `/` (the whole local primary). */
|
|
97
|
+
root?: string;
|
|
98
|
+
/** Report what would be deleted without unlinking anything. */
|
|
99
|
+
dryRun?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Directories (and their subtrees) whose `.keep` files are NEVER deleted.
|
|
102
|
+
* Defaults to `['/.meta']` to protect the intentional backends placeholder
|
|
103
|
+
* that keeps `/.meta/backends` alive.
|
|
104
|
+
*/
|
|
105
|
+
protectedDirs?: string[];
|
|
106
|
+
}
|
|
107
|
+
interface KeepPurgeResult {
|
|
108
|
+
/** Files visited during the walk (`.keep` included). */
|
|
109
|
+
scanned: number;
|
|
110
|
+
/** Placeholders deleted — or, in dryRun, that would be deleted. */
|
|
111
|
+
removed: string[];
|
|
112
|
+
/** Placeholders that could not be deleted (unlink failed). */
|
|
113
|
+
failed: string[];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* True when a file name is the internal `.keep` directory placeholder.
|
|
117
|
+
*
|
|
118
|
+
* Matches exactly `.keep` (the file name), which is the only form produced by
|
|
119
|
+
* the backends — never a suffix match, so `foo.keep` is left untouched.
|
|
120
|
+
*/
|
|
121
|
+
declare function isKeepFile(fileName: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Delete every leaked `.keep` placeholder stored in a local primary backend.
|
|
124
|
+
*
|
|
125
|
+
* Walks the whole tree from `options.root` — including dotfiles and `/.meta/`
|
|
126
|
+
* entries — but skips any directory listed in `protectedDirs` (default
|
|
127
|
+
* `/.meta`) so the intentional backends placeholder survives. Unreadable
|
|
128
|
+
* entries are skipped; failures on individual files are collected in `failed`
|
|
129
|
+
* instead of aborting the walk.
|
|
130
|
+
*/
|
|
131
|
+
declare function purgeKeepFiles(fs: PurgeableFS, options?: PurgeKeepOptions): Promise<KeepPurgeResult>;
|
|
65
132
|
|
|
66
133
|
/** A single backend in the topology. */
|
|
67
134
|
interface BackendDescriptor {
|
|
@@ -229,6 +296,18 @@ interface ConfigRepoOptions {
|
|
|
229
296
|
* Default: `true`. Pass `false` to skip the (local-only) startup sweep.
|
|
230
297
|
*/
|
|
231
298
|
purgeMtimeSidecars?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* Delete leaked `.keep` placeholders from the local primary on startup.
|
|
301
|
+
*
|
|
302
|
+
* Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
|
|
303
|
+
* directory alive with an internal `.keep` placeholder. Older builds copied
|
|
304
|
+
* those backend-internal files into the local primary, where sync ignores
|
|
305
|
+
* them — so they linger forever. The intentional `/.meta/backends/.keep` is
|
|
306
|
+
* always protected.
|
|
307
|
+
*
|
|
308
|
+
* Default: `true`. Pass `false` to skip the (local-only) startup sweep.
|
|
309
|
+
*/
|
|
310
|
+
purgeKeepFiles?: boolean;
|
|
232
311
|
/**
|
|
233
312
|
* Migrate legacy `.x.version` sidecars to the new `<name>.version` naming on
|
|
234
313
|
* startup (rename locally, delete the remote residual).
|
|
@@ -433,6 +512,17 @@ interface IConfigRepo {
|
|
|
433
512
|
root?: string;
|
|
434
513
|
dryRun?: boolean;
|
|
435
514
|
}): Promise<MtimePurgeResult>;
|
|
515
|
+
/**
|
|
516
|
+
* Delete leaked `.keep` placeholders from the local primary backend.
|
|
517
|
+
*
|
|
518
|
+
* Runs automatically on `createConfigRepo()` unless
|
|
519
|
+
* `options.purgeKeepFiles === false`. Call it manually to clean a
|
|
520
|
+
* long-lived store, or with `{ dryRun: true }` to only list them.
|
|
521
|
+
*/
|
|
522
|
+
purgeKeepFiles(options?: {
|
|
523
|
+
root?: string;
|
|
524
|
+
dryRun?: boolean;
|
|
525
|
+
}): Promise<KeepPurgeResult>;
|
|
436
526
|
/**
|
|
437
527
|
* Create a data-sync group for this app.
|
|
438
528
|
* Each backend can optionally reference a config-sync backend's account
|
|
@@ -767,6 +857,19 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
767
857
|
* @param options.dryRun List the sidecars without deleting them.
|
|
768
858
|
*/
|
|
769
859
|
purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
860
|
+
/**
|
|
861
|
+
* Delete leaked `.keep` placeholders from the local primary backend.
|
|
862
|
+
*
|
|
863
|
+
* Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
|
|
864
|
+
* directory alive with an internal `.keep` placeholder. Older builds copied
|
|
865
|
+
* those backend-internal files into the local primary, where sync ignores
|
|
866
|
+
* them — so they linger forever. `/.meta/backends/.keep` (intentional) is
|
|
867
|
+
* protected and never removed.
|
|
868
|
+
*
|
|
869
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
870
|
+
* @param options.dryRun List the placeholders without deleting them.
|
|
871
|
+
*/
|
|
872
|
+
purgeKeepFiles(options?: PurgeKeepOptions): Promise<KeepPurgeResult>;
|
|
770
873
|
/**
|
|
771
874
|
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
772
875
|
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
@@ -1020,4 +1123,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
1020
1123
|
*/
|
|
1021
1124
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
1022
1125
|
|
|
1023
|
-
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 MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
|
1126
|
+
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, type KeepPurgeResult, LOCAL_IDB_BACKEND_ID, type MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeKeepOptions, type PurgeMtimeOptions, type PurgeableFS$1 as PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isKeepFile, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeKeepFiles, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { SyncMapStore, SyncMapTransaction } from '@zenfs/core';
|
|
|
25
25
|
* deleting them would cost network calls and lose mtime precision.
|
|
26
26
|
*/
|
|
27
27
|
/** Minimal async FS surface needed for the purge. */
|
|
28
|
-
interface PurgeableFS {
|
|
28
|
+
interface PurgeableFS$1 {
|
|
29
29
|
readdir(path: string): Promise<string[]>;
|
|
30
30
|
stat(path: string): Promise<{
|
|
31
31
|
mode?: number;
|
|
@@ -61,7 +61,74 @@ declare function isMtimeSidecar(fileName: string): boolean;
|
|
|
61
61
|
* Unreadable entries are skipped; failures on individual files are collected
|
|
62
62
|
* in `failed` instead of aborting the walk.
|
|
63
63
|
*/
|
|
64
|
-
declare function purgeMtimeSidecars(fs: PurgeableFS, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
64
|
+
declare function purgeMtimeSidecars(fs: PurgeableFS$1, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* zen-fs-config — `.keep` placeholder cleanup (local primary only)
|
|
68
|
+
*
|
|
69
|
+
* Backends that cannot store empty directories (Git, RemoteStorage…) keep a
|
|
70
|
+
* directory alive with an internal `.keep` placeholder — exactly like the
|
|
71
|
+
* `.mtime` sidecar, this is backend-internal metadata that MUST be hidden from
|
|
72
|
+
* callers (see zen-fs-sync/docs/SyncableFS.md §1/§2).
|
|
73
|
+
*
|
|
74
|
+
* Older builds leaked `.keep` files into the local primary (IndexedDB on
|
|
75
|
+
* browser, Folder on Node), where they linger forever: the sync engine now
|
|
76
|
+
* skips `.keep` paths on both sides, so nothing ever removes them, and every
|
|
77
|
+
* walk re-creates confusion about which directories are "real".
|
|
78
|
+
*
|
|
79
|
+
* This module walks the local primary and deletes those leaked `.keep` files.
|
|
80
|
+
* It never touches replica (remote) backends: their placeholders are live
|
|
81
|
+
* metadata that keep real (empty) directories alive, and deleting them would
|
|
82
|
+
* lose directory structure.
|
|
83
|
+
*
|
|
84
|
+
* The intentional `.keep` under `/.meta/` (used to keep the backends directory
|
|
85
|
+
* alive) is protected and never deleted — see `protectedDirs`.
|
|
86
|
+
*/
|
|
87
|
+
/** Minimal async FS surface needed for the purge. */
|
|
88
|
+
interface PurgeableFS {
|
|
89
|
+
readdir(path: string): Promise<string[]>;
|
|
90
|
+
stat(path: string): Promise<{
|
|
91
|
+
mode?: number;
|
|
92
|
+
}>;
|
|
93
|
+
unlink(path: string): Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
interface PurgeKeepOptions {
|
|
96
|
+
/** Root to scan. Default: `/` (the whole local primary). */
|
|
97
|
+
root?: string;
|
|
98
|
+
/** Report what would be deleted without unlinking anything. */
|
|
99
|
+
dryRun?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Directories (and their subtrees) whose `.keep` files are NEVER deleted.
|
|
102
|
+
* Defaults to `['/.meta']` to protect the intentional backends placeholder
|
|
103
|
+
* that keeps `/.meta/backends` alive.
|
|
104
|
+
*/
|
|
105
|
+
protectedDirs?: string[];
|
|
106
|
+
}
|
|
107
|
+
interface KeepPurgeResult {
|
|
108
|
+
/** Files visited during the walk (`.keep` included). */
|
|
109
|
+
scanned: number;
|
|
110
|
+
/** Placeholders deleted — or, in dryRun, that would be deleted. */
|
|
111
|
+
removed: string[];
|
|
112
|
+
/** Placeholders that could not be deleted (unlink failed). */
|
|
113
|
+
failed: string[];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* True when a file name is the internal `.keep` directory placeholder.
|
|
117
|
+
*
|
|
118
|
+
* Matches exactly `.keep` (the file name), which is the only form produced by
|
|
119
|
+
* the backends — never a suffix match, so `foo.keep` is left untouched.
|
|
120
|
+
*/
|
|
121
|
+
declare function isKeepFile(fileName: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Delete every leaked `.keep` placeholder stored in a local primary backend.
|
|
124
|
+
*
|
|
125
|
+
* Walks the whole tree from `options.root` — including dotfiles and `/.meta/`
|
|
126
|
+
* entries — but skips any directory listed in `protectedDirs` (default
|
|
127
|
+
* `/.meta`) so the intentional backends placeholder survives. Unreadable
|
|
128
|
+
* entries are skipped; failures on individual files are collected in `failed`
|
|
129
|
+
* instead of aborting the walk.
|
|
130
|
+
*/
|
|
131
|
+
declare function purgeKeepFiles(fs: PurgeableFS, options?: PurgeKeepOptions): Promise<KeepPurgeResult>;
|
|
65
132
|
|
|
66
133
|
/** A single backend in the topology. */
|
|
67
134
|
interface BackendDescriptor {
|
|
@@ -229,6 +296,18 @@ interface ConfigRepoOptions {
|
|
|
229
296
|
* Default: `true`. Pass `false` to skip the (local-only) startup sweep.
|
|
230
297
|
*/
|
|
231
298
|
purgeMtimeSidecars?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* Delete leaked `.keep` placeholders from the local primary on startup.
|
|
301
|
+
*
|
|
302
|
+
* Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
|
|
303
|
+
* directory alive with an internal `.keep` placeholder. Older builds copied
|
|
304
|
+
* those backend-internal files into the local primary, where sync ignores
|
|
305
|
+
* them — so they linger forever. The intentional `/.meta/backends/.keep` is
|
|
306
|
+
* always protected.
|
|
307
|
+
*
|
|
308
|
+
* Default: `true`. Pass `false` to skip the (local-only) startup sweep.
|
|
309
|
+
*/
|
|
310
|
+
purgeKeepFiles?: boolean;
|
|
232
311
|
/**
|
|
233
312
|
* Migrate legacy `.x.version` sidecars to the new `<name>.version` naming on
|
|
234
313
|
* startup (rename locally, delete the remote residual).
|
|
@@ -433,6 +512,17 @@ interface IConfigRepo {
|
|
|
433
512
|
root?: string;
|
|
434
513
|
dryRun?: boolean;
|
|
435
514
|
}): Promise<MtimePurgeResult>;
|
|
515
|
+
/**
|
|
516
|
+
* Delete leaked `.keep` placeholders from the local primary backend.
|
|
517
|
+
*
|
|
518
|
+
* Runs automatically on `createConfigRepo()` unless
|
|
519
|
+
* `options.purgeKeepFiles === false`. Call it manually to clean a
|
|
520
|
+
* long-lived store, or with `{ dryRun: true }` to only list them.
|
|
521
|
+
*/
|
|
522
|
+
purgeKeepFiles(options?: {
|
|
523
|
+
root?: string;
|
|
524
|
+
dryRun?: boolean;
|
|
525
|
+
}): Promise<KeepPurgeResult>;
|
|
436
526
|
/**
|
|
437
527
|
* Create a data-sync group for this app.
|
|
438
528
|
* Each backend can optionally reference a config-sync backend's account
|
|
@@ -767,6 +857,19 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
767
857
|
* @param options.dryRun List the sidecars without deleting them.
|
|
768
858
|
*/
|
|
769
859
|
purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
860
|
+
/**
|
|
861
|
+
* Delete leaked `.keep` placeholders from the local primary backend.
|
|
862
|
+
*
|
|
863
|
+
* Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
|
|
864
|
+
* directory alive with an internal `.keep` placeholder. Older builds copied
|
|
865
|
+
* those backend-internal files into the local primary, where sync ignores
|
|
866
|
+
* them — so they linger forever. `/.meta/backends/.keep` (intentional) is
|
|
867
|
+
* protected and never removed.
|
|
868
|
+
*
|
|
869
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
870
|
+
* @param options.dryRun List the placeholders without deleting them.
|
|
871
|
+
*/
|
|
872
|
+
purgeKeepFiles(options?: PurgeKeepOptions): Promise<KeepPurgeResult>;
|
|
770
873
|
/**
|
|
771
874
|
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
772
875
|
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
@@ -1020,4 +1123,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
1020
1123
|
*/
|
|
1021
1124
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
1022
1125
|
|
|
1023
|
-
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 MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
|
1126
|
+
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, type KeepPurgeResult, LOCAL_IDB_BACKEND_ID, type MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeKeepOptions, type PurgeMtimeOptions, type PurgeableFS$1 as PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isKeepFile, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeKeepFiles, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
package/dist/index.js
CHANGED
|
@@ -385,12 +385,23 @@ function wrapWithCache(backend, backendId, options) {
|
|
|
385
385
|
} else {
|
|
386
386
|
store = new import_zen_fs_cache.MemoryCacheStore();
|
|
387
387
|
}
|
|
388
|
+
try {
|
|
389
|
+
const purge = store.purgeKeepFiles?.();
|
|
390
|
+
if (purge && typeof purge.catch === "function") {
|
|
391
|
+
purge.catch(() => {
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
388
396
|
const wrapped = new import_zen_fs_cache.CachedFileSystem(backend, store, {
|
|
389
397
|
ttlMs: options.ttlMs ?? 0
|
|
390
398
|
});
|
|
391
399
|
if (typeof backend.shouldSync === "function") {
|
|
392
400
|
wrapped.shouldSync = (...args) => backend.shouldSync(...args);
|
|
393
401
|
}
|
|
402
|
+
if (typeof backend.writeFileWithMtime === "function") {
|
|
403
|
+
wrapped.writeFileWithMtime = (path, data, mtimeMs) => backend.writeFileWithMtime(path, data, mtimeMs);
|
|
404
|
+
}
|
|
394
405
|
return wrapped;
|
|
395
406
|
}
|
|
396
407
|
var import_zen_fs_cache;
|
|
@@ -418,6 +429,7 @@ __export(index_exports, {
|
|
|
418
429
|
hasBackend: () => hasBackend,
|
|
419
430
|
incrementVersion: () => incrementVersion,
|
|
420
431
|
isBrowserEnv: () => isBrowserEnv,
|
|
432
|
+
isKeepFile: () => isKeepFile,
|
|
421
433
|
isMtimeSidecar: () => isMtimeSidecar,
|
|
422
434
|
legacyVersionPathFor: () => legacyVersionPathFor,
|
|
423
435
|
listBackendMetadata: () => listBackendMetadata,
|
|
@@ -425,6 +437,7 @@ __export(index_exports, {
|
|
|
425
437
|
localPrimaryType: () => localPrimaryType,
|
|
426
438
|
mergeAccountFields: () => mergeAccountFields,
|
|
427
439
|
migrateVersionSidecars: () => migrateVersionSidecars,
|
|
440
|
+
purgeKeepFiles: () => purgeKeepFiles,
|
|
428
441
|
purgeMtimeSidecars: () => purgeMtimeSidecars,
|
|
429
442
|
readVersion: () => readVersion,
|
|
430
443
|
registerBackend: () => registerBackend,
|
|
@@ -1117,15 +1130,79 @@ async function purgeMtimeSidecars(fs, options = {}) {
|
|
|
1117
1130
|
return result;
|
|
1118
1131
|
}
|
|
1119
1132
|
|
|
1120
|
-
// src/
|
|
1133
|
+
// src/keep-cleanup.ts
|
|
1134
|
+
function isKeepFile(fileName) {
|
|
1135
|
+
return fileName === ".keep";
|
|
1136
|
+
}
|
|
1121
1137
|
var S_IFDIR2 = 16384;
|
|
1122
|
-
|
|
1138
|
+
var DEFAULT_PROTECTED_DIRS = ["/.meta"];
|
|
1139
|
+
function isDirectory2(stat) {
|
|
1123
1140
|
return typeof stat.mode === "number" && (stat.mode & S_IFDIR2) === S_IFDIR2;
|
|
1124
1141
|
}
|
|
1125
1142
|
function joinPath2(dir, entry) {
|
|
1126
1143
|
if (dir === "/" || dir === "") return `/${entry}`;
|
|
1127
1144
|
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
1128
1145
|
}
|
|
1146
|
+
function isProtected(fullPath, protectedDirs) {
|
|
1147
|
+
return protectedDirs.some((d) => {
|
|
1148
|
+
const dir = d.endsWith("/") ? d.slice(0, -1) : d;
|
|
1149
|
+
return fullPath === dir || fullPath.startsWith(dir + "/");
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
async function purgeKeepFiles(fs, options = {}) {
|
|
1153
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
1154
|
+
const dryRun = options.dryRun ?? false;
|
|
1155
|
+
const protectedDirs = options.protectedDirs ?? DEFAULT_PROTECTED_DIRS;
|
|
1156
|
+
const result = { scanned: 0, removed: [], failed: [] };
|
|
1157
|
+
const stack = [root];
|
|
1158
|
+
while (stack.length > 0) {
|
|
1159
|
+
const dir = stack.pop();
|
|
1160
|
+
if (isProtected(dir, protectedDirs)) continue;
|
|
1161
|
+
let entries;
|
|
1162
|
+
try {
|
|
1163
|
+
entries = await fs.readdir(dir);
|
|
1164
|
+
} catch {
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
for (const entry of entries) {
|
|
1168
|
+
const fullPath = joinPath2(dir, entry);
|
|
1169
|
+
if (isProtected(fullPath, protectedDirs)) continue;
|
|
1170
|
+
let stat;
|
|
1171
|
+
try {
|
|
1172
|
+
stat = await fs.stat(fullPath);
|
|
1173
|
+
} catch {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (isDirectory2(stat)) {
|
|
1177
|
+
stack.push(fullPath);
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
result.scanned++;
|
|
1181
|
+
if (!isKeepFile(entry)) continue;
|
|
1182
|
+
if (dryRun) {
|
|
1183
|
+
result.removed.push(fullPath);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
await fs.unlink(fullPath);
|
|
1188
|
+
result.removed.push(fullPath);
|
|
1189
|
+
} catch {
|
|
1190
|
+
result.failed.push(fullPath);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return result;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/version-migration.ts
|
|
1198
|
+
var S_IFDIR3 = 16384;
|
|
1199
|
+
function isDir(stat) {
|
|
1200
|
+
return typeof stat.mode === "number" && (stat.mode & S_IFDIR3) === S_IFDIR3;
|
|
1201
|
+
}
|
|
1202
|
+
function joinPath3(dir, entry) {
|
|
1203
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
1204
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
1205
|
+
}
|
|
1129
1206
|
async function existsPath(fs, p) {
|
|
1130
1207
|
try {
|
|
1131
1208
|
await fs.stat(p);
|
|
@@ -1140,8 +1217,8 @@ function configNameFromLegacy(entry) {
|
|
|
1140
1217
|
}
|
|
1141
1218
|
async function resolveTarget(fs, dir, entry) {
|
|
1142
1219
|
const configName = configNameFromLegacy(entry);
|
|
1143
|
-
const configPath =
|
|
1144
|
-
const dotConfigPath =
|
|
1220
|
+
const configPath = joinPath3(dir, configName);
|
|
1221
|
+
const dotConfigPath = joinPath3(dir, `.${configName}`);
|
|
1145
1222
|
if (await existsPath(fs, dotConfigPath)) {
|
|
1146
1223
|
return { action: "skip" };
|
|
1147
1224
|
}
|
|
@@ -1170,7 +1247,7 @@ async function migrateVersionSidecars(fs, options = {}) {
|
|
|
1170
1247
|
continue;
|
|
1171
1248
|
}
|
|
1172
1249
|
for (const entry of entries) {
|
|
1173
|
-
const full =
|
|
1250
|
+
const full = joinPath3(dir, entry);
|
|
1174
1251
|
let stat;
|
|
1175
1252
|
try {
|
|
1176
1253
|
stat = await fs.stat(full);
|
|
@@ -1724,6 +1801,32 @@ var ConfigRepo = class {
|
|
|
1724
1801
|
}
|
|
1725
1802
|
return result;
|
|
1726
1803
|
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Delete leaked `.keep` placeholders from the local primary backend.
|
|
1806
|
+
*
|
|
1807
|
+
* Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
|
|
1808
|
+
* directory alive with an internal `.keep` placeholder. Older builds copied
|
|
1809
|
+
* those backend-internal files into the local primary, where sync ignores
|
|
1810
|
+
* them — so they linger forever. `/.meta/backends/.keep` (intentional) is
|
|
1811
|
+
* protected and never removed.
|
|
1812
|
+
*
|
|
1813
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
1814
|
+
* @param options.dryRun List the placeholders without deleting them.
|
|
1815
|
+
*/
|
|
1816
|
+
async purgeKeepFiles(options) {
|
|
1817
|
+
this.assertNotDisposed();
|
|
1818
|
+
const result = await purgeKeepFiles(this.cachedFS, options);
|
|
1819
|
+
if (result.removed.length > 0) {
|
|
1820
|
+
log2.log(
|
|
1821
|
+
`[ConfigRepo] purged ${result.removed.length} .keep placeholder(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
|
|
1822
|
+
result.removed
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
if (result.failed.length > 0) {
|
|
1826
|
+
log2.warn(`[ConfigRepo] failed to purge ${result.failed.length} .keep placeholder(s):`, result.failed);
|
|
1827
|
+
}
|
|
1828
|
+
return result;
|
|
1829
|
+
}
|
|
1727
1830
|
/**
|
|
1728
1831
|
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
1729
1832
|
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
@@ -2694,6 +2797,17 @@ var AppDataGroupImpl = class {
|
|
|
2694
2797
|
} catch (err) {
|
|
2695
2798
|
log2.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err?.message ?? err);
|
|
2696
2799
|
}
|
|
2800
|
+
try {
|
|
2801
|
+
const purgedKeep = await purgeKeepFiles(this.localFS);
|
|
2802
|
+
if (purgedKeep.removed.length > 0) {
|
|
2803
|
+
log2.warn(
|
|
2804
|
+
`[AppDataGroup:${this.groupId}] purged ${purgedKeep.removed.length} leaked .keep placeholder(s):`,
|
|
2805
|
+
purgedKeep.removed
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
log2.warn(`[AppDataGroup:${this.groupId}] .keep purge failed:`, err?.message ?? err);
|
|
2810
|
+
}
|
|
2697
2811
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
2698
2812
|
this.fs = createChrootFS(this.localFS, "/");
|
|
2699
2813
|
const { kept, removed } = dedupeAppDataBackends(this._backends);
|
|
@@ -2833,6 +2947,19 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
2833
2947
|
log2.warn(`[createConfigRepo] .mtime purge failed:`, err?.message ?? err);
|
|
2834
2948
|
}
|
|
2835
2949
|
}
|
|
2950
|
+
if (options.purgeKeepFiles !== false) {
|
|
2951
|
+
try {
|
|
2952
|
+
const purged = await purgeKeepFiles(cachedFS);
|
|
2953
|
+
if (purged.removed.length > 0) {
|
|
2954
|
+
log2.warn(
|
|
2955
|
+
`[createConfigRepo] purged ${purged.removed.length} leaked .keep placeholder(s) from local primary:`,
|
|
2956
|
+
purged.removed
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
} catch (err) {
|
|
2960
|
+
log2.warn(`[createConfigRepo] .keep purge failed:`, err?.message ?? err);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2836
2963
|
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
2837
2964
|
try {
|
|
2838
2965
|
await primaryInstance.mkdir(META_DIR);
|
|
@@ -3077,6 +3204,7 @@ init_backend_registry();
|
|
|
3077
3204
|
hasBackend,
|
|
3078
3205
|
incrementVersion,
|
|
3079
3206
|
isBrowserEnv,
|
|
3207
|
+
isKeepFile,
|
|
3080
3208
|
isMtimeSidecar,
|
|
3081
3209
|
legacyVersionPathFor,
|
|
3082
3210
|
listBackendMetadata,
|
|
@@ -3084,6 +3212,7 @@ init_backend_registry();
|
|
|
3084
3212
|
localPrimaryType,
|
|
3085
3213
|
mergeAccountFields,
|
|
3086
3214
|
migrateVersionSidecars,
|
|
3215
|
+
purgeKeepFiles,
|
|
3087
3216
|
purgeMtimeSidecars,
|
|
3088
3217
|
readVersion,
|
|
3089
3218
|
registerBackend,
|
package/dist/index.mjs
CHANGED
|
@@ -688,15 +688,79 @@ async function purgeMtimeSidecars(fs, options = {}) {
|
|
|
688
688
|
return result;
|
|
689
689
|
}
|
|
690
690
|
|
|
691
|
-
// src/
|
|
691
|
+
// src/keep-cleanup.ts
|
|
692
|
+
function isKeepFile(fileName) {
|
|
693
|
+
return fileName === ".keep";
|
|
694
|
+
}
|
|
692
695
|
var S_IFDIR2 = 16384;
|
|
693
|
-
|
|
696
|
+
var DEFAULT_PROTECTED_DIRS = ["/.meta"];
|
|
697
|
+
function isDirectory2(stat) {
|
|
694
698
|
return typeof stat.mode === "number" && (stat.mode & S_IFDIR2) === S_IFDIR2;
|
|
695
699
|
}
|
|
696
700
|
function joinPath2(dir, entry) {
|
|
697
701
|
if (dir === "/" || dir === "") return `/${entry}`;
|
|
698
702
|
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
699
703
|
}
|
|
704
|
+
function isProtected(fullPath, protectedDirs) {
|
|
705
|
+
return protectedDirs.some((d) => {
|
|
706
|
+
const dir = d.endsWith("/") ? d.slice(0, -1) : d;
|
|
707
|
+
return fullPath === dir || fullPath.startsWith(dir + "/");
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
async function purgeKeepFiles(fs, options = {}) {
|
|
711
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
712
|
+
const dryRun = options.dryRun ?? false;
|
|
713
|
+
const protectedDirs = options.protectedDirs ?? DEFAULT_PROTECTED_DIRS;
|
|
714
|
+
const result = { scanned: 0, removed: [], failed: [] };
|
|
715
|
+
const stack = [root];
|
|
716
|
+
while (stack.length > 0) {
|
|
717
|
+
const dir = stack.pop();
|
|
718
|
+
if (isProtected(dir, protectedDirs)) continue;
|
|
719
|
+
let entries;
|
|
720
|
+
try {
|
|
721
|
+
entries = await fs.readdir(dir);
|
|
722
|
+
} catch {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
for (const entry of entries) {
|
|
726
|
+
const fullPath = joinPath2(dir, entry);
|
|
727
|
+
if (isProtected(fullPath, protectedDirs)) continue;
|
|
728
|
+
let stat;
|
|
729
|
+
try {
|
|
730
|
+
stat = await fs.stat(fullPath);
|
|
731
|
+
} catch {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (isDirectory2(stat)) {
|
|
735
|
+
stack.push(fullPath);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
result.scanned++;
|
|
739
|
+
if (!isKeepFile(entry)) continue;
|
|
740
|
+
if (dryRun) {
|
|
741
|
+
result.removed.push(fullPath);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
await fs.unlink(fullPath);
|
|
746
|
+
result.removed.push(fullPath);
|
|
747
|
+
} catch {
|
|
748
|
+
result.failed.push(fullPath);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/version-migration.ts
|
|
756
|
+
var S_IFDIR3 = 16384;
|
|
757
|
+
function isDir(stat) {
|
|
758
|
+
return typeof stat.mode === "number" && (stat.mode & S_IFDIR3) === S_IFDIR3;
|
|
759
|
+
}
|
|
760
|
+
function joinPath3(dir, entry) {
|
|
761
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
762
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
763
|
+
}
|
|
700
764
|
async function existsPath(fs, p) {
|
|
701
765
|
try {
|
|
702
766
|
await fs.stat(p);
|
|
@@ -711,8 +775,8 @@ function configNameFromLegacy(entry) {
|
|
|
711
775
|
}
|
|
712
776
|
async function resolveTarget(fs, dir, entry) {
|
|
713
777
|
const configName = configNameFromLegacy(entry);
|
|
714
|
-
const configPath =
|
|
715
|
-
const dotConfigPath =
|
|
778
|
+
const configPath = joinPath3(dir, configName);
|
|
779
|
+
const dotConfigPath = joinPath3(dir, `.${configName}`);
|
|
716
780
|
if (await existsPath(fs, dotConfigPath)) {
|
|
717
781
|
return { action: "skip" };
|
|
718
782
|
}
|
|
@@ -741,7 +805,7 @@ async function migrateVersionSidecars(fs, options = {}) {
|
|
|
741
805
|
continue;
|
|
742
806
|
}
|
|
743
807
|
for (const entry of entries) {
|
|
744
|
-
const full =
|
|
808
|
+
const full = joinPath3(dir, entry);
|
|
745
809
|
let stat;
|
|
746
810
|
try {
|
|
747
811
|
stat = await fs.stat(full);
|
|
@@ -1295,6 +1359,32 @@ var ConfigRepo = class {
|
|
|
1295
1359
|
}
|
|
1296
1360
|
return result;
|
|
1297
1361
|
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Delete leaked `.keep` placeholders from the local primary backend.
|
|
1364
|
+
*
|
|
1365
|
+
* Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
|
|
1366
|
+
* directory alive with an internal `.keep` placeholder. Older builds copied
|
|
1367
|
+
* those backend-internal files into the local primary, where sync ignores
|
|
1368
|
+
* them — so they linger forever. `/.meta/backends/.keep` (intentional) is
|
|
1369
|
+
* protected and never removed.
|
|
1370
|
+
*
|
|
1371
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
1372
|
+
* @param options.dryRun List the placeholders without deleting them.
|
|
1373
|
+
*/
|
|
1374
|
+
async purgeKeepFiles(options) {
|
|
1375
|
+
this.assertNotDisposed();
|
|
1376
|
+
const result = await purgeKeepFiles(this.cachedFS, options);
|
|
1377
|
+
if (result.removed.length > 0) {
|
|
1378
|
+
log2.log(
|
|
1379
|
+
`[ConfigRepo] purged ${result.removed.length} .keep placeholder(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
|
|
1380
|
+
result.removed
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
if (result.failed.length > 0) {
|
|
1384
|
+
log2.warn(`[ConfigRepo] failed to purge ${result.failed.length} .keep placeholder(s):`, result.failed);
|
|
1385
|
+
}
|
|
1386
|
+
return result;
|
|
1387
|
+
}
|
|
1298
1388
|
/**
|
|
1299
1389
|
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
1300
1390
|
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
@@ -1545,7 +1635,7 @@ var ConfigRepo = class {
|
|
|
1545
1635
|
const instance = await createBackend(desc);
|
|
1546
1636
|
let fsInstance = instance;
|
|
1547
1637
|
if (this.cacheOptions) {
|
|
1548
|
-
const { wrapWithCache } = await import("./cache-wrapper-
|
|
1638
|
+
const { wrapWithCache } = await import("./cache-wrapper-S5HRLRQX.mjs");
|
|
1549
1639
|
fsInstance = wrapWithCache(instance, desc.id, this.cacheOptions);
|
|
1550
1640
|
log2.log(`[ConfigRepo] Replica ${desc.id} wrapped with CachedFileSystem (store=${this.cacheOptions.storeType ?? "IdbCacheStore"})`);
|
|
1551
1641
|
}
|
|
@@ -2265,6 +2355,17 @@ var AppDataGroupImpl = class {
|
|
|
2265
2355
|
} catch (err) {
|
|
2266
2356
|
log2.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err?.message ?? err);
|
|
2267
2357
|
}
|
|
2358
|
+
try {
|
|
2359
|
+
const purgedKeep = await purgeKeepFiles(this.localFS);
|
|
2360
|
+
if (purgedKeep.removed.length > 0) {
|
|
2361
|
+
log2.warn(
|
|
2362
|
+
`[AppDataGroup:${this.groupId}] purged ${purgedKeep.removed.length} leaked .keep placeholder(s):`,
|
|
2363
|
+
purgedKeep.removed
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
} catch (err) {
|
|
2367
|
+
log2.warn(`[AppDataGroup:${this.groupId}] .keep purge failed:`, err?.message ?? err);
|
|
2368
|
+
}
|
|
2268
2369
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
2269
2370
|
this.fs = createChrootFS(this.localFS, "/");
|
|
2270
2371
|
const { kept, removed } = dedupeAppDataBackends(this._backends);
|
|
@@ -2404,6 +2505,19 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
2404
2505
|
log2.warn(`[createConfigRepo] .mtime purge failed:`, err?.message ?? err);
|
|
2405
2506
|
}
|
|
2406
2507
|
}
|
|
2508
|
+
if (options.purgeKeepFiles !== false) {
|
|
2509
|
+
try {
|
|
2510
|
+
const purged = await purgeKeepFiles(cachedFS);
|
|
2511
|
+
if (purged.removed.length > 0) {
|
|
2512
|
+
log2.warn(
|
|
2513
|
+
`[createConfigRepo] purged ${purged.removed.length} leaked .keep placeholder(s) from local primary:`,
|
|
2514
|
+
purged.removed
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
} catch (err) {
|
|
2518
|
+
log2.warn(`[createConfigRepo] .keep purge failed:`, err?.message ?? err);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2407
2521
|
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
2408
2522
|
try {
|
|
2409
2523
|
await primaryInstance.mkdir(META_DIR);
|
|
@@ -2643,6 +2757,7 @@ export {
|
|
|
2643
2757
|
hasBackend,
|
|
2644
2758
|
incrementVersion,
|
|
2645
2759
|
isBrowserEnv,
|
|
2760
|
+
isKeepFile,
|
|
2646
2761
|
isMtimeSidecar,
|
|
2647
2762
|
legacyVersionPathFor,
|
|
2648
2763
|
listBackendMetadata,
|
|
@@ -2650,6 +2765,7 @@ export {
|
|
|
2650
2765
|
localPrimaryType,
|
|
2651
2766
|
mergeAccountFields,
|
|
2652
2767
|
migrateVersionSidecars,
|
|
2768
|
+
purgeKeepFiles,
|
|
2653
2769
|
purgeMtimeSidecars,
|
|
2654
2770
|
readVersion,
|
|
2655
2771
|
registerBackend,
|
package/dist/zen-fs-config.js
CHANGED
|
@@ -24886,10 +24886,35 @@ var ZenFSConfig = (() => {
|
|
|
24886
24886
|
}
|
|
24887
24887
|
async exists(path) {
|
|
24888
24888
|
try {
|
|
24889
|
+
const innerType = this.inner?.constructor?.name ?? typeof this.inner;
|
|
24890
|
+
const hasStat = typeof this.inner?.stat === "function";
|
|
24891
|
+
const hasExists = typeof this.inner?.exists === "function";
|
|
24892
|
+
const backendName = this.inner?.backendName ?? this.inner?.constructor?.name ?? "unknown";
|
|
24893
|
+
console.log(`[CACHE-EXISTS] exists(${path}): inner=${innerType} backend=${backendName} hasStat=${hasStat} hasExists=${hasExists}`);
|
|
24894
|
+
const statKey = `stat:${path}`;
|
|
24895
|
+
const cachedStat = await this.store.get(statKey);
|
|
24896
|
+
if (cachedStat) {
|
|
24897
|
+
const age = Date.now() - cachedStat.cachedAt;
|
|
24898
|
+
console.log(`[CACHE-EXISTS] exists(${path}): cached stat entry found, age=${age}ms, ttlMs=${this.ttlMs}`);
|
|
24899
|
+
} else {
|
|
24900
|
+
console.log(`[CACHE-EXISTS] exists(${path}): no cached stat entry`);
|
|
24901
|
+
}
|
|
24902
|
+
if (hasExists) {
|
|
24903
|
+
console.log(`[CACHE-EXISTS] exists(${path}): \u2192 calling inner.exists() [backend=${backendName}]`);
|
|
24904
|
+
const result = await this.inner.exists(path);
|
|
24905
|
+
console.log(`[CACHE-EXISTS] exists(${path}): \u2190 inner.exists() [backend=${backendName}] \u2192 ${result}`);
|
|
24906
|
+
return result;
|
|
24907
|
+
}
|
|
24908
|
+
console.log(`[CACHE-EXISTS] exists(${path}): no inner.exists(), falling back to inner.stat() [backend=${backendName}]`);
|
|
24889
24909
|
await this.inner.stat(path);
|
|
24910
|
+
console.log(`[CACHE-EXISTS] exists(${path}): \u2190 inner.stat() [backend=${backendName}] \u2192 OK (exists)`);
|
|
24890
24911
|
return true;
|
|
24891
24912
|
} catch (err2) {
|
|
24892
|
-
if (this.isNotFound(err2))
|
|
24913
|
+
if (this.isNotFound(err2)) {
|
|
24914
|
+
console.log(`[CACHE-EXISTS] exists(${path}): \u2190 inner.stat() \u2192 NotFound`);
|
|
24915
|
+
return false;
|
|
24916
|
+
}
|
|
24917
|
+
console.log(`[CACHE-EXISTS] exists(${path}): \u2190 inner.stat() \u2192 ERROR: ${err2}`);
|
|
24893
24918
|
throw err2;
|
|
24894
24919
|
}
|
|
24895
24920
|
}
|
|
@@ -24922,7 +24947,10 @@ var ZenFSConfig = (() => {
|
|
|
24922
24947
|
await this.invalidateMeta(path);
|
|
24923
24948
|
}
|
|
24924
24949
|
async unlink(path) {
|
|
24950
|
+
const backendName = this.inner?.backendName ?? this.inner?.constructor?.name ?? "unknown";
|
|
24951
|
+
console.log(`[CACHE-UNLINK] unlink(${path}): calling inner.unlink() [backend=${backendName}]`);
|
|
24925
24952
|
await this.inner.unlink(path);
|
|
24953
|
+
console.log(`[CACHE-UNLINK] unlink(${path}): inner.unlink() OK [backend=${backendName}], invalidating cache`);
|
|
24926
24954
|
await this.invalidate(path);
|
|
24927
24955
|
}
|
|
24928
24956
|
async mkdir(path, options) {
|
|
@@ -24981,6 +25009,12 @@ var ZenFSConfig = (() => {
|
|
|
24981
25009
|
async clear() {
|
|
24982
25010
|
this.store.clear();
|
|
24983
25011
|
}
|
|
25012
|
+
async purgeKeepFiles() {
|
|
25013
|
+
for (const key of Array.from(this.store.keys())) {
|
|
25014
|
+
const base = key === "/" ? "" : key.split("/").pop() ?? "";
|
|
25015
|
+
if (base === ".keep") this.store.delete(key);
|
|
25016
|
+
}
|
|
25017
|
+
}
|
|
24984
25018
|
};
|
|
24985
25019
|
IdbCacheStore = class {
|
|
24986
25020
|
constructor(prefix = "zen-fs-cache:", dbName = "zen-fs-cache", storeName = "cache") {
|
|
@@ -25061,6 +25095,28 @@ var ZenFSConfig = (() => {
|
|
|
25061
25095
|
} catch {
|
|
25062
25096
|
}
|
|
25063
25097
|
}
|
|
25098
|
+
async purgeKeepFiles() {
|
|
25099
|
+
try {
|
|
25100
|
+
const db = await this.open();
|
|
25101
|
+
await new Promise((resolve4, reject) => {
|
|
25102
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
25103
|
+
const store = tx.objectStore(this.storeName);
|
|
25104
|
+
const req = store.getAllKeys();
|
|
25105
|
+
req.onsuccess = () => {
|
|
25106
|
+
const keys = req.result ?? [];
|
|
25107
|
+
for (const raw of keys) {
|
|
25108
|
+
const path = raw.startsWith(this.prefix) ? raw.slice(this.prefix.length) : raw;
|
|
25109
|
+
const base = path === "/" ? "" : path.split("/").pop() ?? "";
|
|
25110
|
+
if (base === ".keep") store.delete(raw);
|
|
25111
|
+
}
|
|
25112
|
+
};
|
|
25113
|
+
req.onerror = () => reject(req.error);
|
|
25114
|
+
tx.oncomplete = () => resolve4();
|
|
25115
|
+
tx.onerror = () => reject(tx.error);
|
|
25116
|
+
});
|
|
25117
|
+
} catch {
|
|
25118
|
+
}
|
|
25119
|
+
}
|
|
25064
25120
|
};
|
|
25065
25121
|
}
|
|
25066
25122
|
});
|
|
@@ -25079,12 +25135,23 @@ var ZenFSConfig = (() => {
|
|
|
25079
25135
|
} else {
|
|
25080
25136
|
store = new MemoryCacheStore();
|
|
25081
25137
|
}
|
|
25138
|
+
try {
|
|
25139
|
+
const purge = store.purgeKeepFiles?.();
|
|
25140
|
+
if (purge && typeof purge.catch === "function") {
|
|
25141
|
+
purge.catch(() => {
|
|
25142
|
+
});
|
|
25143
|
+
}
|
|
25144
|
+
} catch {
|
|
25145
|
+
}
|
|
25082
25146
|
const wrapped = new CachedFileSystem(backend, store, {
|
|
25083
25147
|
ttlMs: options.ttlMs ?? 0
|
|
25084
25148
|
});
|
|
25085
25149
|
if (typeof backend.shouldSync === "function") {
|
|
25086
25150
|
wrapped.shouldSync = (...args) => backend.shouldSync(...args);
|
|
25087
25151
|
}
|
|
25152
|
+
if (typeof backend.writeFileWithMtime === "function") {
|
|
25153
|
+
wrapped.writeFileWithMtime = (path, data, mtimeMs) => backend.writeFileWithMtime(path, data, mtimeMs);
|
|
25154
|
+
}
|
|
25088
25155
|
return wrapped;
|
|
25089
25156
|
}
|
|
25090
25157
|
var init_cache_wrapper = __esm({
|
|
@@ -25111,6 +25178,7 @@ var ZenFSConfig = (() => {
|
|
|
25111
25178
|
hasBackend: () => hasBackend,
|
|
25112
25179
|
incrementVersion: () => incrementVersion,
|
|
25113
25180
|
isBrowserEnv: () => isBrowserEnv,
|
|
25181
|
+
isKeepFile: () => isKeepFile,
|
|
25114
25182
|
isMtimeSidecar: () => isMtimeSidecar,
|
|
25115
25183
|
legacyVersionPathFor: () => legacyVersionPathFor,
|
|
25116
25184
|
listBackendMetadata: () => listBackendMetadata,
|
|
@@ -25118,6 +25186,7 @@ var ZenFSConfig = (() => {
|
|
|
25118
25186
|
localPrimaryType: () => localPrimaryType,
|
|
25119
25187
|
mergeAccountFields: () => mergeAccountFields,
|
|
25120
25188
|
migrateVersionSidecars: () => migrateVersionSidecars,
|
|
25189
|
+
purgeKeepFiles: () => purgeKeepFiles,
|
|
25121
25190
|
purgeMtimeSidecars: () => purgeMtimeSidecars,
|
|
25122
25191
|
readVersion: () => readVersion,
|
|
25123
25192
|
registerBackend: () => registerBackend,
|
|
@@ -26994,15 +27063,79 @@ var ZenFSConfig = (() => {
|
|
|
26994
27063
|
return result;
|
|
26995
27064
|
}
|
|
26996
27065
|
|
|
26997
|
-
// src/
|
|
27066
|
+
// src/keep-cleanup.ts
|
|
27067
|
+
function isKeepFile(fileName) {
|
|
27068
|
+
return fileName === ".keep";
|
|
27069
|
+
}
|
|
26998
27070
|
var S_IFDIR3 = 16384;
|
|
26999
|
-
|
|
27071
|
+
var DEFAULT_PROTECTED_DIRS = ["/.meta"];
|
|
27072
|
+
function isDirectory4(stat5) {
|
|
27000
27073
|
return typeof stat5.mode === "number" && (stat5.mode & S_IFDIR3) === S_IFDIR3;
|
|
27001
27074
|
}
|
|
27002
27075
|
function joinPath2(dir, entry) {
|
|
27003
27076
|
if (dir === "/" || dir === "") return `/${entry}`;
|
|
27004
27077
|
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
27005
27078
|
}
|
|
27079
|
+
function isProtected(fullPath, protectedDirs) {
|
|
27080
|
+
return protectedDirs.some((d) => {
|
|
27081
|
+
const dir = d.endsWith("/") ? d.slice(0, -1) : d;
|
|
27082
|
+
return fullPath === dir || fullPath.startsWith(dir + "/");
|
|
27083
|
+
});
|
|
27084
|
+
}
|
|
27085
|
+
async function purgeKeepFiles(fs, options = {}) {
|
|
27086
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
27087
|
+
const dryRun = options.dryRun ?? false;
|
|
27088
|
+
const protectedDirs = options.protectedDirs ?? DEFAULT_PROTECTED_DIRS;
|
|
27089
|
+
const result = { scanned: 0, removed: [], failed: [] };
|
|
27090
|
+
const stack = [root];
|
|
27091
|
+
while (stack.length > 0) {
|
|
27092
|
+
const dir = stack.pop();
|
|
27093
|
+
if (isProtected(dir, protectedDirs)) continue;
|
|
27094
|
+
let entries2;
|
|
27095
|
+
try {
|
|
27096
|
+
entries2 = await fs.readdir(dir);
|
|
27097
|
+
} catch {
|
|
27098
|
+
continue;
|
|
27099
|
+
}
|
|
27100
|
+
for (const entry of entries2) {
|
|
27101
|
+
const fullPath = joinPath2(dir, entry);
|
|
27102
|
+
if (isProtected(fullPath, protectedDirs)) continue;
|
|
27103
|
+
let stat5;
|
|
27104
|
+
try {
|
|
27105
|
+
stat5 = await fs.stat(fullPath);
|
|
27106
|
+
} catch {
|
|
27107
|
+
continue;
|
|
27108
|
+
}
|
|
27109
|
+
if (isDirectory4(stat5)) {
|
|
27110
|
+
stack.push(fullPath);
|
|
27111
|
+
continue;
|
|
27112
|
+
}
|
|
27113
|
+
result.scanned++;
|
|
27114
|
+
if (!isKeepFile(entry)) continue;
|
|
27115
|
+
if (dryRun) {
|
|
27116
|
+
result.removed.push(fullPath);
|
|
27117
|
+
continue;
|
|
27118
|
+
}
|
|
27119
|
+
try {
|
|
27120
|
+
await fs.unlink(fullPath);
|
|
27121
|
+
result.removed.push(fullPath);
|
|
27122
|
+
} catch {
|
|
27123
|
+
result.failed.push(fullPath);
|
|
27124
|
+
}
|
|
27125
|
+
}
|
|
27126
|
+
}
|
|
27127
|
+
return result;
|
|
27128
|
+
}
|
|
27129
|
+
|
|
27130
|
+
// src/version-migration.ts
|
|
27131
|
+
var S_IFDIR4 = 16384;
|
|
27132
|
+
function isDir(stat5) {
|
|
27133
|
+
return typeof stat5.mode === "number" && (stat5.mode & S_IFDIR4) === S_IFDIR4;
|
|
27134
|
+
}
|
|
27135
|
+
function joinPath3(dir, entry) {
|
|
27136
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
27137
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
27138
|
+
}
|
|
27006
27139
|
async function existsPath(fs, p) {
|
|
27007
27140
|
try {
|
|
27008
27141
|
await fs.stat(p);
|
|
@@ -27017,8 +27150,8 @@ var ZenFSConfig = (() => {
|
|
|
27017
27150
|
}
|
|
27018
27151
|
async function resolveTarget(fs, dir, entry) {
|
|
27019
27152
|
const configName = configNameFromLegacy(entry);
|
|
27020
|
-
const configPath =
|
|
27021
|
-
const dotConfigPath =
|
|
27153
|
+
const configPath = joinPath3(dir, configName);
|
|
27154
|
+
const dotConfigPath = joinPath3(dir, `.${configName}`);
|
|
27022
27155
|
if (await existsPath(fs, dotConfigPath)) {
|
|
27023
27156
|
return { action: "skip" };
|
|
27024
27157
|
}
|
|
@@ -27047,7 +27180,7 @@ var ZenFSConfig = (() => {
|
|
|
27047
27180
|
continue;
|
|
27048
27181
|
}
|
|
27049
27182
|
for (const entry of entries2) {
|
|
27050
|
-
const full =
|
|
27183
|
+
const full = joinPath3(dir, entry);
|
|
27051
27184
|
let stat5;
|
|
27052
27185
|
try {
|
|
27053
27186
|
stat5 = await fs.stat(full);
|
|
@@ -27601,6 +27734,32 @@ var ZenFSConfig = (() => {
|
|
|
27601
27734
|
}
|
|
27602
27735
|
return result;
|
|
27603
27736
|
}
|
|
27737
|
+
/**
|
|
27738
|
+
* Delete leaked `.keep` placeholders from the local primary backend.
|
|
27739
|
+
*
|
|
27740
|
+
* Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
|
|
27741
|
+
* directory alive with an internal `.keep` placeholder. Older builds copied
|
|
27742
|
+
* those backend-internal files into the local primary, where sync ignores
|
|
27743
|
+
* them — so they linger forever. `/.meta/backends/.keep` (intentional) is
|
|
27744
|
+
* protected and never removed.
|
|
27745
|
+
*
|
|
27746
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
27747
|
+
* @param options.dryRun List the placeholders without deleting them.
|
|
27748
|
+
*/
|
|
27749
|
+
async purgeKeepFiles(options) {
|
|
27750
|
+
this.assertNotDisposed();
|
|
27751
|
+
const result = await purgeKeepFiles(this.cachedFS, options);
|
|
27752
|
+
if (result.removed.length > 0) {
|
|
27753
|
+
log6.log(
|
|
27754
|
+
`[ConfigRepo] purged ${result.removed.length} .keep placeholder(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
|
|
27755
|
+
result.removed
|
|
27756
|
+
);
|
|
27757
|
+
}
|
|
27758
|
+
if (result.failed.length > 0) {
|
|
27759
|
+
log6.warn(`[ConfigRepo] failed to purge ${result.failed.length} .keep placeholder(s):`, result.failed);
|
|
27760
|
+
}
|
|
27761
|
+
return result;
|
|
27762
|
+
}
|
|
27604
27763
|
/**
|
|
27605
27764
|
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
27606
27765
|
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
@@ -28571,6 +28730,17 @@ var ZenFSConfig = (() => {
|
|
|
28571
28730
|
} catch (err2) {
|
|
28572
28731
|
log6.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err2?.message ?? err2);
|
|
28573
28732
|
}
|
|
28733
|
+
try {
|
|
28734
|
+
const purgedKeep = await purgeKeepFiles(this.localFS);
|
|
28735
|
+
if (purgedKeep.removed.length > 0) {
|
|
28736
|
+
log6.warn(
|
|
28737
|
+
`[AppDataGroup:${this.groupId}] purged ${purgedKeep.removed.length} leaked .keep placeholder(s):`,
|
|
28738
|
+
purgedKeep.removed
|
|
28739
|
+
);
|
|
28740
|
+
}
|
|
28741
|
+
} catch (err2) {
|
|
28742
|
+
log6.warn(`[AppDataGroup:${this.groupId}] .keep purge failed:`, err2?.message ?? err2);
|
|
28743
|
+
}
|
|
28574
28744
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
28575
28745
|
this.fs = createChrootFS(this.localFS, "/");
|
|
28576
28746
|
const { kept, removed } = dedupeAppDataBackends(this._backends);
|
|
@@ -28710,6 +28880,19 @@ var ZenFSConfig = (() => {
|
|
|
28710
28880
|
log6.warn(`[createConfigRepo] .mtime purge failed:`, err2?.message ?? err2);
|
|
28711
28881
|
}
|
|
28712
28882
|
}
|
|
28883
|
+
if (options.purgeKeepFiles !== false) {
|
|
28884
|
+
try {
|
|
28885
|
+
const purged = await purgeKeepFiles(cachedFS);
|
|
28886
|
+
if (purged.removed.length > 0) {
|
|
28887
|
+
log6.warn(
|
|
28888
|
+
`[createConfigRepo] purged ${purged.removed.length} leaked .keep placeholder(s) from local primary:`,
|
|
28889
|
+
purged.removed
|
|
28890
|
+
);
|
|
28891
|
+
}
|
|
28892
|
+
} catch (err2) {
|
|
28893
|
+
log6.warn(`[createConfigRepo] .keep purge failed:`, err2?.message ?? err2);
|
|
28894
|
+
}
|
|
28895
|
+
}
|
|
28713
28896
|
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
28714
28897
|
try {
|
|
28715
28898
|
await primaryInstance.mkdir(META_DIR);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-fs-config",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.36",
|
|
4
4
|
"description": "Distributed config management library built on ZenFS, zen-fs-cache, and zen-fs-sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"tsup": "^8.5.1",
|
|
81
81
|
"typescript": "^5.9.3",
|
|
82
82
|
"vitest": "^1.6.1",
|
|
83
|
-
"zen-fs-cache": "^1.0.
|
|
83
|
+
"zen-fs-cache": "^1.0.6",
|
|
84
84
|
"zen-fs-sync": "^0.4.9"
|
|
85
85
|
}
|
|
86
86
|
}
|