zen-fs-config 0.5.33 → 0.5.34
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/index.d.mts +106 -3
- package/dist/index.d.ts +106 -3
- package/dist/index.js +123 -5
- package/dist/index.mjs +121 -5
- package/dist/zen-fs-config.js +121 -5
- package/package.json +1 -1
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
|
@@ -418,6 +418,7 @@ __export(index_exports, {
|
|
|
418
418
|
hasBackend: () => hasBackend,
|
|
419
419
|
incrementVersion: () => incrementVersion,
|
|
420
420
|
isBrowserEnv: () => isBrowserEnv,
|
|
421
|
+
isKeepFile: () => isKeepFile,
|
|
421
422
|
isMtimeSidecar: () => isMtimeSidecar,
|
|
422
423
|
legacyVersionPathFor: () => legacyVersionPathFor,
|
|
423
424
|
listBackendMetadata: () => listBackendMetadata,
|
|
@@ -425,6 +426,7 @@ __export(index_exports, {
|
|
|
425
426
|
localPrimaryType: () => localPrimaryType,
|
|
426
427
|
mergeAccountFields: () => mergeAccountFields,
|
|
427
428
|
migrateVersionSidecars: () => migrateVersionSidecars,
|
|
429
|
+
purgeKeepFiles: () => purgeKeepFiles,
|
|
428
430
|
purgeMtimeSidecars: () => purgeMtimeSidecars,
|
|
429
431
|
readVersion: () => readVersion,
|
|
430
432
|
registerBackend: () => registerBackend,
|
|
@@ -1117,15 +1119,79 @@ async function purgeMtimeSidecars(fs, options = {}) {
|
|
|
1117
1119
|
return result;
|
|
1118
1120
|
}
|
|
1119
1121
|
|
|
1120
|
-
// src/
|
|
1122
|
+
// src/keep-cleanup.ts
|
|
1123
|
+
function isKeepFile(fileName) {
|
|
1124
|
+
return fileName === ".keep";
|
|
1125
|
+
}
|
|
1121
1126
|
var S_IFDIR2 = 16384;
|
|
1122
|
-
|
|
1127
|
+
var DEFAULT_PROTECTED_DIRS = ["/.meta"];
|
|
1128
|
+
function isDirectory2(stat) {
|
|
1123
1129
|
return typeof stat.mode === "number" && (stat.mode & S_IFDIR2) === S_IFDIR2;
|
|
1124
1130
|
}
|
|
1125
1131
|
function joinPath2(dir, entry) {
|
|
1126
1132
|
if (dir === "/" || dir === "") return `/${entry}`;
|
|
1127
1133
|
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
1128
1134
|
}
|
|
1135
|
+
function isProtected(fullPath, protectedDirs) {
|
|
1136
|
+
return protectedDirs.some((d) => {
|
|
1137
|
+
const dir = d.endsWith("/") ? d.slice(0, -1) : d;
|
|
1138
|
+
return fullPath === dir || fullPath.startsWith(dir + "/");
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
async function purgeKeepFiles(fs, options = {}) {
|
|
1142
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
1143
|
+
const dryRun = options.dryRun ?? false;
|
|
1144
|
+
const protectedDirs = options.protectedDirs ?? DEFAULT_PROTECTED_DIRS;
|
|
1145
|
+
const result = { scanned: 0, removed: [], failed: [] };
|
|
1146
|
+
const stack = [root];
|
|
1147
|
+
while (stack.length > 0) {
|
|
1148
|
+
const dir = stack.pop();
|
|
1149
|
+
if (isProtected(dir, protectedDirs)) continue;
|
|
1150
|
+
let entries;
|
|
1151
|
+
try {
|
|
1152
|
+
entries = await fs.readdir(dir);
|
|
1153
|
+
} catch {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
for (const entry of entries) {
|
|
1157
|
+
const fullPath = joinPath2(dir, entry);
|
|
1158
|
+
if (isProtected(fullPath, protectedDirs)) continue;
|
|
1159
|
+
let stat;
|
|
1160
|
+
try {
|
|
1161
|
+
stat = await fs.stat(fullPath);
|
|
1162
|
+
} catch {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (isDirectory2(stat)) {
|
|
1166
|
+
stack.push(fullPath);
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
result.scanned++;
|
|
1170
|
+
if (!isKeepFile(entry)) continue;
|
|
1171
|
+
if (dryRun) {
|
|
1172
|
+
result.removed.push(fullPath);
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
await fs.unlink(fullPath);
|
|
1177
|
+
result.removed.push(fullPath);
|
|
1178
|
+
} catch {
|
|
1179
|
+
result.failed.push(fullPath);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return result;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/version-migration.ts
|
|
1187
|
+
var S_IFDIR3 = 16384;
|
|
1188
|
+
function isDir(stat) {
|
|
1189
|
+
return typeof stat.mode === "number" && (stat.mode & S_IFDIR3) === S_IFDIR3;
|
|
1190
|
+
}
|
|
1191
|
+
function joinPath3(dir, entry) {
|
|
1192
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
1193
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
1194
|
+
}
|
|
1129
1195
|
async function existsPath(fs, p) {
|
|
1130
1196
|
try {
|
|
1131
1197
|
await fs.stat(p);
|
|
@@ -1140,8 +1206,8 @@ function configNameFromLegacy(entry) {
|
|
|
1140
1206
|
}
|
|
1141
1207
|
async function resolveTarget(fs, dir, entry) {
|
|
1142
1208
|
const configName = configNameFromLegacy(entry);
|
|
1143
|
-
const configPath =
|
|
1144
|
-
const dotConfigPath =
|
|
1209
|
+
const configPath = joinPath3(dir, configName);
|
|
1210
|
+
const dotConfigPath = joinPath3(dir, `.${configName}`);
|
|
1145
1211
|
if (await existsPath(fs, dotConfigPath)) {
|
|
1146
1212
|
return { action: "skip" };
|
|
1147
1213
|
}
|
|
@@ -1170,7 +1236,7 @@ async function migrateVersionSidecars(fs, options = {}) {
|
|
|
1170
1236
|
continue;
|
|
1171
1237
|
}
|
|
1172
1238
|
for (const entry of entries) {
|
|
1173
|
-
const full =
|
|
1239
|
+
const full = joinPath3(dir, entry);
|
|
1174
1240
|
let stat;
|
|
1175
1241
|
try {
|
|
1176
1242
|
stat = await fs.stat(full);
|
|
@@ -1724,6 +1790,32 @@ var ConfigRepo = class {
|
|
|
1724
1790
|
}
|
|
1725
1791
|
return result;
|
|
1726
1792
|
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Delete leaked `.keep` placeholders from the local primary backend.
|
|
1795
|
+
*
|
|
1796
|
+
* Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
|
|
1797
|
+
* directory alive with an internal `.keep` placeholder. Older builds copied
|
|
1798
|
+
* those backend-internal files into the local primary, where sync ignores
|
|
1799
|
+
* them — so they linger forever. `/.meta/backends/.keep` (intentional) is
|
|
1800
|
+
* protected and never removed.
|
|
1801
|
+
*
|
|
1802
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
1803
|
+
* @param options.dryRun List the placeholders without deleting them.
|
|
1804
|
+
*/
|
|
1805
|
+
async purgeKeepFiles(options) {
|
|
1806
|
+
this.assertNotDisposed();
|
|
1807
|
+
const result = await purgeKeepFiles(this.cachedFS, options);
|
|
1808
|
+
if (result.removed.length > 0) {
|
|
1809
|
+
log2.log(
|
|
1810
|
+
`[ConfigRepo] purged ${result.removed.length} .keep placeholder(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
|
|
1811
|
+
result.removed
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
if (result.failed.length > 0) {
|
|
1815
|
+
log2.warn(`[ConfigRepo] failed to purge ${result.failed.length} .keep placeholder(s):`, result.failed);
|
|
1816
|
+
}
|
|
1817
|
+
return result;
|
|
1818
|
+
}
|
|
1727
1819
|
/**
|
|
1728
1820
|
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
1729
1821
|
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
@@ -2694,6 +2786,17 @@ var AppDataGroupImpl = class {
|
|
|
2694
2786
|
} catch (err) {
|
|
2695
2787
|
log2.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err?.message ?? err);
|
|
2696
2788
|
}
|
|
2789
|
+
try {
|
|
2790
|
+
const purgedKeep = await purgeKeepFiles(this.localFS);
|
|
2791
|
+
if (purgedKeep.removed.length > 0) {
|
|
2792
|
+
log2.warn(
|
|
2793
|
+
`[AppDataGroup:${this.groupId}] purged ${purgedKeep.removed.length} leaked .keep placeholder(s):`,
|
|
2794
|
+
purgedKeep.removed
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
} catch (err) {
|
|
2798
|
+
log2.warn(`[AppDataGroup:${this.groupId}] .keep purge failed:`, err?.message ?? err);
|
|
2799
|
+
}
|
|
2697
2800
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
2698
2801
|
this.fs = createChrootFS(this.localFS, "/");
|
|
2699
2802
|
const { kept, removed } = dedupeAppDataBackends(this._backends);
|
|
@@ -2833,6 +2936,19 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
2833
2936
|
log2.warn(`[createConfigRepo] .mtime purge failed:`, err?.message ?? err);
|
|
2834
2937
|
}
|
|
2835
2938
|
}
|
|
2939
|
+
if (options.purgeKeepFiles !== false) {
|
|
2940
|
+
try {
|
|
2941
|
+
const purged = await purgeKeepFiles(cachedFS);
|
|
2942
|
+
if (purged.removed.length > 0) {
|
|
2943
|
+
log2.warn(
|
|
2944
|
+
`[createConfigRepo] purged ${purged.removed.length} leaked .keep placeholder(s) from local primary:`,
|
|
2945
|
+
purged.removed
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
} catch (err) {
|
|
2949
|
+
log2.warn(`[createConfigRepo] .keep purge failed:`, err?.message ?? err);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2836
2952
|
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
2837
2953
|
try {
|
|
2838
2954
|
await primaryInstance.mkdir(META_DIR);
|
|
@@ -3077,6 +3193,7 @@ init_backend_registry();
|
|
|
3077
3193
|
hasBackend,
|
|
3078
3194
|
incrementVersion,
|
|
3079
3195
|
isBrowserEnv,
|
|
3196
|
+
isKeepFile,
|
|
3080
3197
|
isMtimeSidecar,
|
|
3081
3198
|
legacyVersionPathFor,
|
|
3082
3199
|
listBackendMetadata,
|
|
@@ -3084,6 +3201,7 @@ init_backend_registry();
|
|
|
3084
3201
|
localPrimaryType,
|
|
3085
3202
|
mergeAccountFields,
|
|
3086
3203
|
migrateVersionSidecars,
|
|
3204
|
+
purgeKeepFiles,
|
|
3087
3205
|
purgeMtimeSidecars,
|
|
3088
3206
|
readVersion,
|
|
3089
3207
|
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)
|
|
@@ -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
|
@@ -25111,6 +25111,7 @@ var ZenFSConfig = (() => {
|
|
|
25111
25111
|
hasBackend: () => hasBackend,
|
|
25112
25112
|
incrementVersion: () => incrementVersion,
|
|
25113
25113
|
isBrowserEnv: () => isBrowserEnv,
|
|
25114
|
+
isKeepFile: () => isKeepFile,
|
|
25114
25115
|
isMtimeSidecar: () => isMtimeSidecar,
|
|
25115
25116
|
legacyVersionPathFor: () => legacyVersionPathFor,
|
|
25116
25117
|
listBackendMetadata: () => listBackendMetadata,
|
|
@@ -25118,6 +25119,7 @@ var ZenFSConfig = (() => {
|
|
|
25118
25119
|
localPrimaryType: () => localPrimaryType,
|
|
25119
25120
|
mergeAccountFields: () => mergeAccountFields,
|
|
25120
25121
|
migrateVersionSidecars: () => migrateVersionSidecars,
|
|
25122
|
+
purgeKeepFiles: () => purgeKeepFiles,
|
|
25121
25123
|
purgeMtimeSidecars: () => purgeMtimeSidecars,
|
|
25122
25124
|
readVersion: () => readVersion,
|
|
25123
25125
|
registerBackend: () => registerBackend,
|
|
@@ -26994,15 +26996,79 @@ var ZenFSConfig = (() => {
|
|
|
26994
26996
|
return result;
|
|
26995
26997
|
}
|
|
26996
26998
|
|
|
26997
|
-
// src/
|
|
26999
|
+
// src/keep-cleanup.ts
|
|
27000
|
+
function isKeepFile(fileName) {
|
|
27001
|
+
return fileName === ".keep";
|
|
27002
|
+
}
|
|
26998
27003
|
var S_IFDIR3 = 16384;
|
|
26999
|
-
|
|
27004
|
+
var DEFAULT_PROTECTED_DIRS = ["/.meta"];
|
|
27005
|
+
function isDirectory4(stat5) {
|
|
27000
27006
|
return typeof stat5.mode === "number" && (stat5.mode & S_IFDIR3) === S_IFDIR3;
|
|
27001
27007
|
}
|
|
27002
27008
|
function joinPath2(dir, entry) {
|
|
27003
27009
|
if (dir === "/" || dir === "") return `/${entry}`;
|
|
27004
27010
|
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
27005
27011
|
}
|
|
27012
|
+
function isProtected(fullPath, protectedDirs) {
|
|
27013
|
+
return protectedDirs.some((d) => {
|
|
27014
|
+
const dir = d.endsWith("/") ? d.slice(0, -1) : d;
|
|
27015
|
+
return fullPath === dir || fullPath.startsWith(dir + "/");
|
|
27016
|
+
});
|
|
27017
|
+
}
|
|
27018
|
+
async function purgeKeepFiles(fs, options = {}) {
|
|
27019
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
27020
|
+
const dryRun = options.dryRun ?? false;
|
|
27021
|
+
const protectedDirs = options.protectedDirs ?? DEFAULT_PROTECTED_DIRS;
|
|
27022
|
+
const result = { scanned: 0, removed: [], failed: [] };
|
|
27023
|
+
const stack = [root];
|
|
27024
|
+
while (stack.length > 0) {
|
|
27025
|
+
const dir = stack.pop();
|
|
27026
|
+
if (isProtected(dir, protectedDirs)) continue;
|
|
27027
|
+
let entries2;
|
|
27028
|
+
try {
|
|
27029
|
+
entries2 = await fs.readdir(dir);
|
|
27030
|
+
} catch {
|
|
27031
|
+
continue;
|
|
27032
|
+
}
|
|
27033
|
+
for (const entry of entries2) {
|
|
27034
|
+
const fullPath = joinPath2(dir, entry);
|
|
27035
|
+
if (isProtected(fullPath, protectedDirs)) continue;
|
|
27036
|
+
let stat5;
|
|
27037
|
+
try {
|
|
27038
|
+
stat5 = await fs.stat(fullPath);
|
|
27039
|
+
} catch {
|
|
27040
|
+
continue;
|
|
27041
|
+
}
|
|
27042
|
+
if (isDirectory4(stat5)) {
|
|
27043
|
+
stack.push(fullPath);
|
|
27044
|
+
continue;
|
|
27045
|
+
}
|
|
27046
|
+
result.scanned++;
|
|
27047
|
+
if (!isKeepFile(entry)) continue;
|
|
27048
|
+
if (dryRun) {
|
|
27049
|
+
result.removed.push(fullPath);
|
|
27050
|
+
continue;
|
|
27051
|
+
}
|
|
27052
|
+
try {
|
|
27053
|
+
await fs.unlink(fullPath);
|
|
27054
|
+
result.removed.push(fullPath);
|
|
27055
|
+
} catch {
|
|
27056
|
+
result.failed.push(fullPath);
|
|
27057
|
+
}
|
|
27058
|
+
}
|
|
27059
|
+
}
|
|
27060
|
+
return result;
|
|
27061
|
+
}
|
|
27062
|
+
|
|
27063
|
+
// src/version-migration.ts
|
|
27064
|
+
var S_IFDIR4 = 16384;
|
|
27065
|
+
function isDir(stat5) {
|
|
27066
|
+
return typeof stat5.mode === "number" && (stat5.mode & S_IFDIR4) === S_IFDIR4;
|
|
27067
|
+
}
|
|
27068
|
+
function joinPath3(dir, entry) {
|
|
27069
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
27070
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
27071
|
+
}
|
|
27006
27072
|
async function existsPath(fs, p) {
|
|
27007
27073
|
try {
|
|
27008
27074
|
await fs.stat(p);
|
|
@@ -27017,8 +27083,8 @@ var ZenFSConfig = (() => {
|
|
|
27017
27083
|
}
|
|
27018
27084
|
async function resolveTarget(fs, dir, entry) {
|
|
27019
27085
|
const configName = configNameFromLegacy(entry);
|
|
27020
|
-
const configPath =
|
|
27021
|
-
const dotConfigPath =
|
|
27086
|
+
const configPath = joinPath3(dir, configName);
|
|
27087
|
+
const dotConfigPath = joinPath3(dir, `.${configName}`);
|
|
27022
27088
|
if (await existsPath(fs, dotConfigPath)) {
|
|
27023
27089
|
return { action: "skip" };
|
|
27024
27090
|
}
|
|
@@ -27047,7 +27113,7 @@ var ZenFSConfig = (() => {
|
|
|
27047
27113
|
continue;
|
|
27048
27114
|
}
|
|
27049
27115
|
for (const entry of entries2) {
|
|
27050
|
-
const full =
|
|
27116
|
+
const full = joinPath3(dir, entry);
|
|
27051
27117
|
let stat5;
|
|
27052
27118
|
try {
|
|
27053
27119
|
stat5 = await fs.stat(full);
|
|
@@ -27601,6 +27667,32 @@ var ZenFSConfig = (() => {
|
|
|
27601
27667
|
}
|
|
27602
27668
|
return result;
|
|
27603
27669
|
}
|
|
27670
|
+
/**
|
|
27671
|
+
* Delete leaked `.keep` placeholders from the local primary backend.
|
|
27672
|
+
*
|
|
27673
|
+
* Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
|
|
27674
|
+
* directory alive with an internal `.keep` placeholder. Older builds copied
|
|
27675
|
+
* those backend-internal files into the local primary, where sync ignores
|
|
27676
|
+
* them — so they linger forever. `/.meta/backends/.keep` (intentional) is
|
|
27677
|
+
* protected and never removed.
|
|
27678
|
+
*
|
|
27679
|
+
* @param options.root Scan only this subtree (default `/`).
|
|
27680
|
+
* @param options.dryRun List the placeholders without deleting them.
|
|
27681
|
+
*/
|
|
27682
|
+
async purgeKeepFiles(options) {
|
|
27683
|
+
this.assertNotDisposed();
|
|
27684
|
+
const result = await purgeKeepFiles(this.cachedFS, options);
|
|
27685
|
+
if (result.removed.length > 0) {
|
|
27686
|
+
log6.log(
|
|
27687
|
+
`[ConfigRepo] purged ${result.removed.length} .keep placeholder(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
|
|
27688
|
+
result.removed
|
|
27689
|
+
);
|
|
27690
|
+
}
|
|
27691
|
+
if (result.failed.length > 0) {
|
|
27692
|
+
log6.warn(`[ConfigRepo] failed to purge ${result.failed.length} .keep placeholder(s):`, result.failed);
|
|
27693
|
+
}
|
|
27694
|
+
return result;
|
|
27695
|
+
}
|
|
27604
27696
|
/**
|
|
27605
27697
|
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
27606
27698
|
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
@@ -28571,6 +28663,17 @@ var ZenFSConfig = (() => {
|
|
|
28571
28663
|
} catch (err2) {
|
|
28572
28664
|
log6.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err2?.message ?? err2);
|
|
28573
28665
|
}
|
|
28666
|
+
try {
|
|
28667
|
+
const purgedKeep = await purgeKeepFiles(this.localFS);
|
|
28668
|
+
if (purgedKeep.removed.length > 0) {
|
|
28669
|
+
log6.warn(
|
|
28670
|
+
`[AppDataGroup:${this.groupId}] purged ${purgedKeep.removed.length} leaked .keep placeholder(s):`,
|
|
28671
|
+
purgedKeep.removed
|
|
28672
|
+
);
|
|
28673
|
+
}
|
|
28674
|
+
} catch (err2) {
|
|
28675
|
+
log6.warn(`[AppDataGroup:${this.groupId}] .keep purge failed:`, err2?.message ?? err2);
|
|
28676
|
+
}
|
|
28574
28677
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
28575
28678
|
this.fs = createChrootFS(this.localFS, "/");
|
|
28576
28679
|
const { kept, removed } = dedupeAppDataBackends(this._backends);
|
|
@@ -28710,6 +28813,19 @@ var ZenFSConfig = (() => {
|
|
|
28710
28813
|
log6.warn(`[createConfigRepo] .mtime purge failed:`, err2?.message ?? err2);
|
|
28711
28814
|
}
|
|
28712
28815
|
}
|
|
28816
|
+
if (options.purgeKeepFiles !== false) {
|
|
28817
|
+
try {
|
|
28818
|
+
const purged = await purgeKeepFiles(cachedFS);
|
|
28819
|
+
if (purged.removed.length > 0) {
|
|
28820
|
+
log6.warn(
|
|
28821
|
+
`[createConfigRepo] purged ${purged.removed.length} leaked .keep placeholder(s) from local primary:`,
|
|
28822
|
+
purged.removed
|
|
28823
|
+
);
|
|
28824
|
+
}
|
|
28825
|
+
} catch (err2) {
|
|
28826
|
+
log6.warn(`[createConfigRepo] .keep purge failed:`, err2?.message ?? err2);
|
|
28827
|
+
}
|
|
28828
|
+
}
|
|
28713
28829
|
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
28714
28830
|
try {
|
|
28715
28831
|
await primaryInstance.mkdir(META_DIR);
|