zen-fs-config 0.5.32 → 0.5.33
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 -9
- package/dist/index.d.ts +106 -9
- package/dist/index.js +202 -4
- package/dist/index.mjs +200 -4
- package/dist/zen-fs-config.js +212 -16
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -57,7 +57,7 @@ declare function isMtimeSidecar(fileName: string): boolean;
|
|
|
57
57
|
* Delete every `.mtime` sidecar stored in a local primary backend.
|
|
58
58
|
*
|
|
59
59
|
* Walks the whole tree from `options.root` — including dotfiles and `/.meta/`,
|
|
60
|
-
* because sidecars live next to their data file
|
|
60
|
+
* because sidecars live next to their data file as `<name>.mtime` files.
|
|
61
61
|
* Unreadable entries are skipped; failures on individual files are collected
|
|
62
62
|
* in `failed` instead of aborting the walk.
|
|
63
63
|
*/
|
|
@@ -229,6 +229,18 @@ interface ConfigRepoOptions {
|
|
|
229
229
|
* Default: `true`. Pass `false` to skip the (local-only) startup sweep.
|
|
230
230
|
*/
|
|
231
231
|
purgeMtimeSidecars?: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* Migrate legacy `.x.version` sidecars to the new `<name>.version` naming on
|
|
234
|
+
* startup (rename locally, delete the remote residual).
|
|
235
|
+
*
|
|
236
|
+
* Older builds stored version sidecars as hidden dotfiles (`.db.json.version`).
|
|
237
|
+
* They are now `<name>.version`. This one-time sweep renames existing legacy
|
|
238
|
+
* sidecars (preserving version history) and removes the old copy from every
|
|
239
|
+
* replica so sync does not pull it back.
|
|
240
|
+
*
|
|
241
|
+
* Default: `true`. Pass `false` to skip the startup sweep.
|
|
242
|
+
*/
|
|
243
|
+
migrateVersionSidecars?: boolean;
|
|
232
244
|
}
|
|
233
245
|
/** Type of sync group. */
|
|
234
246
|
type SyncGroupType = 'config-sync' | 'data-sync';
|
|
@@ -585,6 +597,71 @@ declare function getAccountFields(type: string): string[];
|
|
|
585
597
|
declare function mergeAccountFields(targetType: string, sourceOptions: Record<string, unknown>, targetOptions: Record<string, unknown>): Record<string, unknown>;
|
|
586
598
|
declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
|
|
587
599
|
|
|
600
|
+
/**
|
|
601
|
+
* zen-fs-config — Legacy Version Sidecar Migration
|
|
602
|
+
*
|
|
603
|
+
* Version sidecars used to be stored as hidden dotfiles:
|
|
604
|
+
*
|
|
605
|
+
* config file: /app-a/db.json
|
|
606
|
+
* version file: /app-a/.db.json.version (OLD — starts with `.`)
|
|
607
|
+
*
|
|
608
|
+
* They are now stored as `<name>.version` (no leading dot):
|
|
609
|
+
*
|
|
610
|
+
* version file: /app-a/db.json.version (NEW)
|
|
611
|
+
*
|
|
612
|
+
* This module migrates existing legacy sidecars to the new naming at startup,
|
|
613
|
+
* preserving version history (the file content is byte-identical after rename).
|
|
614
|
+
*
|
|
615
|
+
* Ambiguity safety: a legacy path `.x.version` could in theory belong to either
|
|
616
|
+
* a dotfile config `.x` (whose version is unchanged by the rename) or a
|
|
617
|
+
* non-dot config `x` (old naming). We resolve it by probing the directory:
|
|
618
|
+
* - `.x` exists → it's the dotfile config's version → NEW path == legacy → skip
|
|
619
|
+
* - `x` exists → rename legacy `.x.version` → `x.version`
|
|
620
|
+
* - neither → orphan (config gone) → delete
|
|
621
|
+
*
|
|
622
|
+
* We also delete the remote residual legacy path from every replica so the
|
|
623
|
+
* sync engine does not pull the old sidecar back (zen-fs-sync syncs `.version`
|
|
624
|
+
* files on both ends).
|
|
625
|
+
*/
|
|
626
|
+
/** Minimal async FS surface needed to read/rename/delete on the local primary. */
|
|
627
|
+
interface MigrateLocalFS {
|
|
628
|
+
readdir(path: string): Promise<string[]>;
|
|
629
|
+
stat(path: string): Promise<{
|
|
630
|
+
mode?: number;
|
|
631
|
+
}>;
|
|
632
|
+
readFile(path: string, encoding?: string): Promise<Uint8Array | string>;
|
|
633
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
634
|
+
unlink(path: string): Promise<void>;
|
|
635
|
+
}
|
|
636
|
+
/** Minimal async FS surface needed to delete remote residual legacy sidecars. */
|
|
637
|
+
interface MigrateReplicaFS {
|
|
638
|
+
unlink(path: string): Promise<void>;
|
|
639
|
+
}
|
|
640
|
+
interface VersionMigrationOptions {
|
|
641
|
+
/** Root to scan. Default: `/` (the whole local primary). */
|
|
642
|
+
root?: string;
|
|
643
|
+
/** Report what would change without touching anything. */
|
|
644
|
+
dryRun?: boolean;
|
|
645
|
+
/** Replica backends whose legacy sidecars should be deleted. */
|
|
646
|
+
replicas?: MigrateReplicaFS[];
|
|
647
|
+
}
|
|
648
|
+
interface VersionMigrationResult {
|
|
649
|
+
/** Local legacy sidecars renamed to the new naming. */
|
|
650
|
+
renamed: string[];
|
|
651
|
+
/** Local orphan legacy sidecars deleted (no owning config). */
|
|
652
|
+
deleted: string[];
|
|
653
|
+
/** Remote residual legacy sidecars deleted (best-effort). */
|
|
654
|
+
remoteDeleted: string[];
|
|
655
|
+
/** Paths that failed to migrate. */
|
|
656
|
+
failed: string[];
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Walk `fs` from `options.root` and migrate every legacy `.x.version` /
|
|
660
|
+
* `.x.version.mtime` sidecar to the new `<name>.version` / `<name>.version.mtime`
|
|
661
|
+
* naming. Best-effort: failures are collected in `failed`, never thrown.
|
|
662
|
+
*/
|
|
663
|
+
declare function migrateVersionSidecars(fs: MigrateLocalFS, options?: VersionMigrationOptions): Promise<VersionMigrationResult>;
|
|
664
|
+
|
|
588
665
|
/**
|
|
589
666
|
* zen-fs-config — ConfigRepo Implementation
|
|
590
667
|
*
|
|
@@ -690,6 +767,15 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
690
767
|
* @param options.dryRun List the sidecars without deleting them.
|
|
691
768
|
*/
|
|
692
769
|
purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
770
|
+
/**
|
|
771
|
+
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
772
|
+
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
773
|
+
* and deletes the residual legacy copy from every replica backend so sync
|
|
774
|
+
* does not pull it back. Best-effort: failures are logged, never thrown.
|
|
775
|
+
*
|
|
776
|
+
* Runs once at startup (see createConfigRepo). Safe to call again.
|
|
777
|
+
*/
|
|
778
|
+
migrateLegacyVersionSidecars(): Promise<VersionMigrationResult>;
|
|
693
779
|
/**
|
|
694
780
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
695
781
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -876,24 +962,35 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
|
|
|
876
962
|
/**
|
|
877
963
|
* zen-fs-config — Sidecar Version File Management
|
|
878
964
|
*
|
|
879
|
-
* Each config file has a companion
|
|
880
|
-
* detection and conflict resolution.
|
|
965
|
+
* Each config file has a companion `.version` sidecar file for version-based
|
|
966
|
+
* change detection and conflict resolution. The sidecar is named `<name>.version`
|
|
967
|
+
* (NOT `.name.version`) so it is not a hidden dotfile.
|
|
881
968
|
*
|
|
882
969
|
* Config file: /app-a/db.json
|
|
883
|
-
* Version file: /app-a
|
|
970
|
+
* Version file: /app-a/db.json.version
|
|
884
971
|
*/
|
|
885
972
|
|
|
886
973
|
/**
|
|
887
974
|
* Compute the sidecar version file path from a config file path.
|
|
888
975
|
*
|
|
889
|
-
* /app-a/db.json → /app-a
|
|
890
|
-
* /shared/flags.json → /shared
|
|
891
|
-
* /nodes/s1/env.json → /nodes/s1
|
|
976
|
+
* /app-a/db.json → /app-a/db.json.version
|
|
977
|
+
* /shared/flags.json → /shared/flags.json.version
|
|
978
|
+
* /nodes/s1/env.json → /nodes/s1/env.json.version
|
|
892
979
|
*
|
|
893
980
|
* Returns null for files that are already version sidecars (.version files),
|
|
894
|
-
* to prevent creating version-of-version files (e.g.
|
|
981
|
+
* to prevent creating version-of-version files (e.g. db.json.version.version).
|
|
895
982
|
*/
|
|
896
983
|
declare function versionPathFor(configFilePath: string): string | null;
|
|
984
|
+
/**
|
|
985
|
+
* Compute the *legacy* (pre-dotfile-removal) version sidecar path for a config
|
|
986
|
+
* file. Returns the old `.${name}.version` form for non-dot configs, or `null`
|
|
987
|
+
* for dotfile configs (whose version path is unchanged) and for version files
|
|
988
|
+
* themselves.
|
|
989
|
+
*
|
|
990
|
+
* Used as a read fallback so existing history in `.db.json.version` is still
|
|
991
|
+
* found after the naming scheme changed to `db.json.version`.
|
|
992
|
+
*/
|
|
993
|
+
declare function legacyVersionPathFor(configFilePath: string): string | null;
|
|
897
994
|
/**
|
|
898
995
|
* Compute SHA-256 hash of a Uint8Array.
|
|
899
996
|
* Returns "sha256:" prefix + hex digest.
|
|
@@ -923,4 +1020,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
923
1020
|
*/
|
|
924
1021
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
925
1022
|
|
|
926
|
-
export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ declare function isMtimeSidecar(fileName: string): boolean;
|
|
|
57
57
|
* Delete every `.mtime` sidecar stored in a local primary backend.
|
|
58
58
|
*
|
|
59
59
|
* Walks the whole tree from `options.root` — including dotfiles and `/.meta/`,
|
|
60
|
-
* because sidecars live next to their data file
|
|
60
|
+
* because sidecars live next to their data file as `<name>.mtime` files.
|
|
61
61
|
* Unreadable entries are skipped; failures on individual files are collected
|
|
62
62
|
* in `failed` instead of aborting the walk.
|
|
63
63
|
*/
|
|
@@ -229,6 +229,18 @@ interface ConfigRepoOptions {
|
|
|
229
229
|
* Default: `true`. Pass `false` to skip the (local-only) startup sweep.
|
|
230
230
|
*/
|
|
231
231
|
purgeMtimeSidecars?: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* Migrate legacy `.x.version` sidecars to the new `<name>.version` naming on
|
|
234
|
+
* startup (rename locally, delete the remote residual).
|
|
235
|
+
*
|
|
236
|
+
* Older builds stored version sidecars as hidden dotfiles (`.db.json.version`).
|
|
237
|
+
* They are now `<name>.version`. This one-time sweep renames existing legacy
|
|
238
|
+
* sidecars (preserving version history) and removes the old copy from every
|
|
239
|
+
* replica so sync does not pull it back.
|
|
240
|
+
*
|
|
241
|
+
* Default: `true`. Pass `false` to skip the startup sweep.
|
|
242
|
+
*/
|
|
243
|
+
migrateVersionSidecars?: boolean;
|
|
232
244
|
}
|
|
233
245
|
/** Type of sync group. */
|
|
234
246
|
type SyncGroupType = 'config-sync' | 'data-sync';
|
|
@@ -585,6 +597,71 @@ declare function getAccountFields(type: string): string[];
|
|
|
585
597
|
declare function mergeAccountFields(targetType: string, sourceOptions: Record<string, unknown>, targetOptions: Record<string, unknown>): Record<string, unknown>;
|
|
586
598
|
declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
|
|
587
599
|
|
|
600
|
+
/**
|
|
601
|
+
* zen-fs-config — Legacy Version Sidecar Migration
|
|
602
|
+
*
|
|
603
|
+
* Version sidecars used to be stored as hidden dotfiles:
|
|
604
|
+
*
|
|
605
|
+
* config file: /app-a/db.json
|
|
606
|
+
* version file: /app-a/.db.json.version (OLD — starts with `.`)
|
|
607
|
+
*
|
|
608
|
+
* They are now stored as `<name>.version` (no leading dot):
|
|
609
|
+
*
|
|
610
|
+
* version file: /app-a/db.json.version (NEW)
|
|
611
|
+
*
|
|
612
|
+
* This module migrates existing legacy sidecars to the new naming at startup,
|
|
613
|
+
* preserving version history (the file content is byte-identical after rename).
|
|
614
|
+
*
|
|
615
|
+
* Ambiguity safety: a legacy path `.x.version` could in theory belong to either
|
|
616
|
+
* a dotfile config `.x` (whose version is unchanged by the rename) or a
|
|
617
|
+
* non-dot config `x` (old naming). We resolve it by probing the directory:
|
|
618
|
+
* - `.x` exists → it's the dotfile config's version → NEW path == legacy → skip
|
|
619
|
+
* - `x` exists → rename legacy `.x.version` → `x.version`
|
|
620
|
+
* - neither → orphan (config gone) → delete
|
|
621
|
+
*
|
|
622
|
+
* We also delete the remote residual legacy path from every replica so the
|
|
623
|
+
* sync engine does not pull the old sidecar back (zen-fs-sync syncs `.version`
|
|
624
|
+
* files on both ends).
|
|
625
|
+
*/
|
|
626
|
+
/** Minimal async FS surface needed to read/rename/delete on the local primary. */
|
|
627
|
+
interface MigrateLocalFS {
|
|
628
|
+
readdir(path: string): Promise<string[]>;
|
|
629
|
+
stat(path: string): Promise<{
|
|
630
|
+
mode?: number;
|
|
631
|
+
}>;
|
|
632
|
+
readFile(path: string, encoding?: string): Promise<Uint8Array | string>;
|
|
633
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
634
|
+
unlink(path: string): Promise<void>;
|
|
635
|
+
}
|
|
636
|
+
/** Minimal async FS surface needed to delete remote residual legacy sidecars. */
|
|
637
|
+
interface MigrateReplicaFS {
|
|
638
|
+
unlink(path: string): Promise<void>;
|
|
639
|
+
}
|
|
640
|
+
interface VersionMigrationOptions {
|
|
641
|
+
/** Root to scan. Default: `/` (the whole local primary). */
|
|
642
|
+
root?: string;
|
|
643
|
+
/** Report what would change without touching anything. */
|
|
644
|
+
dryRun?: boolean;
|
|
645
|
+
/** Replica backends whose legacy sidecars should be deleted. */
|
|
646
|
+
replicas?: MigrateReplicaFS[];
|
|
647
|
+
}
|
|
648
|
+
interface VersionMigrationResult {
|
|
649
|
+
/** Local legacy sidecars renamed to the new naming. */
|
|
650
|
+
renamed: string[];
|
|
651
|
+
/** Local orphan legacy sidecars deleted (no owning config). */
|
|
652
|
+
deleted: string[];
|
|
653
|
+
/** Remote residual legacy sidecars deleted (best-effort). */
|
|
654
|
+
remoteDeleted: string[];
|
|
655
|
+
/** Paths that failed to migrate. */
|
|
656
|
+
failed: string[];
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Walk `fs` from `options.root` and migrate every legacy `.x.version` /
|
|
660
|
+
* `.x.version.mtime` sidecar to the new `<name>.version` / `<name>.version.mtime`
|
|
661
|
+
* naming. Best-effort: failures are collected in `failed`, never thrown.
|
|
662
|
+
*/
|
|
663
|
+
declare function migrateVersionSidecars(fs: MigrateLocalFS, options?: VersionMigrationOptions): Promise<VersionMigrationResult>;
|
|
664
|
+
|
|
588
665
|
/**
|
|
589
666
|
* zen-fs-config — ConfigRepo Implementation
|
|
590
667
|
*
|
|
@@ -690,6 +767,15 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
690
767
|
* @param options.dryRun List the sidecars without deleting them.
|
|
691
768
|
*/
|
|
692
769
|
purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
|
|
770
|
+
/**
|
|
771
|
+
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
772
|
+
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
773
|
+
* and deletes the residual legacy copy from every replica backend so sync
|
|
774
|
+
* does not pull it back. Best-effort: failures are logged, never thrown.
|
|
775
|
+
*
|
|
776
|
+
* Runs once at startup (see createConfigRepo). Safe to call again.
|
|
777
|
+
*/
|
|
778
|
+
migrateLegacyVersionSidecars(): Promise<VersionMigrationResult>;
|
|
693
779
|
/**
|
|
694
780
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
695
781
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -876,24 +962,35 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
|
|
|
876
962
|
/**
|
|
877
963
|
* zen-fs-config — Sidecar Version File Management
|
|
878
964
|
*
|
|
879
|
-
* Each config file has a companion
|
|
880
|
-
* detection and conflict resolution.
|
|
965
|
+
* Each config file has a companion `.version` sidecar file for version-based
|
|
966
|
+
* change detection and conflict resolution. The sidecar is named `<name>.version`
|
|
967
|
+
* (NOT `.name.version`) so it is not a hidden dotfile.
|
|
881
968
|
*
|
|
882
969
|
* Config file: /app-a/db.json
|
|
883
|
-
* Version file: /app-a
|
|
970
|
+
* Version file: /app-a/db.json.version
|
|
884
971
|
*/
|
|
885
972
|
|
|
886
973
|
/**
|
|
887
974
|
* Compute the sidecar version file path from a config file path.
|
|
888
975
|
*
|
|
889
|
-
* /app-a/db.json → /app-a
|
|
890
|
-
* /shared/flags.json → /shared
|
|
891
|
-
* /nodes/s1/env.json → /nodes/s1
|
|
976
|
+
* /app-a/db.json → /app-a/db.json.version
|
|
977
|
+
* /shared/flags.json → /shared/flags.json.version
|
|
978
|
+
* /nodes/s1/env.json → /nodes/s1/env.json.version
|
|
892
979
|
*
|
|
893
980
|
* Returns null for files that are already version sidecars (.version files),
|
|
894
|
-
* to prevent creating version-of-version files (e.g.
|
|
981
|
+
* to prevent creating version-of-version files (e.g. db.json.version.version).
|
|
895
982
|
*/
|
|
896
983
|
declare function versionPathFor(configFilePath: string): string | null;
|
|
984
|
+
/**
|
|
985
|
+
* Compute the *legacy* (pre-dotfile-removal) version sidecar path for a config
|
|
986
|
+
* file. Returns the old `.${name}.version` form for non-dot configs, or `null`
|
|
987
|
+
* for dotfile configs (whose version path is unchanged) and for version files
|
|
988
|
+
* themselves.
|
|
989
|
+
*
|
|
990
|
+
* Used as a read fallback so existing history in `.db.json.version` is still
|
|
991
|
+
* found after the naming scheme changed to `db.json.version`.
|
|
992
|
+
*/
|
|
993
|
+
declare function legacyVersionPathFor(configFilePath: string): string | null;
|
|
897
994
|
/**
|
|
898
995
|
* Compute SHA-256 hash of a Uint8Array.
|
|
899
996
|
* Returns "sha256:" prefix + hex digest.
|
|
@@ -923,4 +1020,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
923
1020
|
*/
|
|
924
1021
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
925
1022
|
|
|
926
|
-
export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -419,10 +419,12 @@ __export(index_exports, {
|
|
|
419
419
|
incrementVersion: () => incrementVersion,
|
|
420
420
|
isBrowserEnv: () => isBrowserEnv,
|
|
421
421
|
isMtimeSidecar: () => isMtimeSidecar,
|
|
422
|
+
legacyVersionPathFor: () => legacyVersionPathFor,
|
|
422
423
|
listBackendMetadata: () => listBackendMetadata,
|
|
423
424
|
listBackends: () => listBackends,
|
|
424
425
|
localPrimaryType: () => localPrimaryType,
|
|
425
426
|
mergeAccountFields: () => mergeAccountFields,
|
|
427
|
+
migrateVersionSidecars: () => migrateVersionSidecars,
|
|
426
428
|
purgeMtimeSidecars: () => purgeMtimeSidecars,
|
|
427
429
|
readVersion: () => readVersion,
|
|
428
430
|
registerBackend: () => registerBackend,
|
|
@@ -828,9 +830,30 @@ function versionPathFor(configFilePath) {
|
|
|
828
830
|
if (fileName.endsWith(".version")) {
|
|
829
831
|
return null;
|
|
830
832
|
}
|
|
831
|
-
const versionFileName =
|
|
833
|
+
const versionFileName = `${fileName}.version`;
|
|
832
834
|
return dir ? `${dir}/${versionFileName}` : versionFileName;
|
|
833
835
|
}
|
|
836
|
+
function legacyVersionPathFor(configFilePath) {
|
|
837
|
+
const lastSlash = configFilePath.lastIndexOf("/");
|
|
838
|
+
const dir = lastSlash >= 0 ? configFilePath.slice(0, lastSlash) : "";
|
|
839
|
+
const fileName = lastSlash >= 0 ? configFilePath.slice(lastSlash + 1) : configFilePath;
|
|
840
|
+
if (fileName.endsWith(".version")) return null;
|
|
841
|
+
if (fileName.startsWith(".")) return null;
|
|
842
|
+
const legacyFileName = `.${fileName}.version`;
|
|
843
|
+
return dir ? `${dir}/${legacyFileName}` : legacyFileName;
|
|
844
|
+
}
|
|
845
|
+
async function readVersionForConfig(fs, configFilePath) {
|
|
846
|
+
const newPath = versionPathFor(configFilePath);
|
|
847
|
+
if (newPath) {
|
|
848
|
+
const v = await readVersion(fs, newPath);
|
|
849
|
+
if (v) return v;
|
|
850
|
+
}
|
|
851
|
+
const legacyPath = legacyVersionPathFor(configFilePath);
|
|
852
|
+
if (legacyPath) {
|
|
853
|
+
return readVersion(fs, legacyPath);
|
|
854
|
+
}
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
834
857
|
function sha256PureJS(data) {
|
|
835
858
|
const K = new Uint32Array([
|
|
836
859
|
1116352408,
|
|
@@ -994,7 +1017,7 @@ async function writeVersion(fs, versionFilePath, meta) {
|
|
|
994
1017
|
}
|
|
995
1018
|
async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
996
1019
|
const vPath = versionPathFor(configFilePath);
|
|
997
|
-
const prev = vPath ? await
|
|
1020
|
+
const prev = vPath ? await readVersionForConfig(fs, configFilePath) : null;
|
|
998
1021
|
const hash = await sha256(newContent);
|
|
999
1022
|
if (prev && prev.hash === hash) {
|
|
1000
1023
|
return prev;
|
|
@@ -1009,7 +1032,7 @@ async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
|
1009
1032
|
async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
1010
1033
|
const vPath = versionPathFor(configFilePath);
|
|
1011
1034
|
if (!vPath) return null;
|
|
1012
|
-
const existing = await
|
|
1035
|
+
const existing = await readVersionForConfig(fs, configFilePath);
|
|
1013
1036
|
if (!existing) return null;
|
|
1014
1037
|
try {
|
|
1015
1038
|
const content = await fs.readFile(configFilePath);
|
|
@@ -1094,6 +1117,131 @@ async function purgeMtimeSidecars(fs, options = {}) {
|
|
|
1094
1117
|
return result;
|
|
1095
1118
|
}
|
|
1096
1119
|
|
|
1120
|
+
// src/version-migration.ts
|
|
1121
|
+
var S_IFDIR2 = 16384;
|
|
1122
|
+
function isDir(stat) {
|
|
1123
|
+
return typeof stat.mode === "number" && (stat.mode & S_IFDIR2) === S_IFDIR2;
|
|
1124
|
+
}
|
|
1125
|
+
function joinPath2(dir, entry) {
|
|
1126
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
1127
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
1128
|
+
}
|
|
1129
|
+
async function existsPath(fs, p) {
|
|
1130
|
+
try {
|
|
1131
|
+
await fs.stat(p);
|
|
1132
|
+
return true;
|
|
1133
|
+
} catch {
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function configNameFromLegacy(entry) {
|
|
1138
|
+
const noMtime = entry.endsWith(".version.mtime") ? entry.slice(0, -".version.mtime".length) : entry.slice(0, -".version".length);
|
|
1139
|
+
return noMtime.slice(1);
|
|
1140
|
+
}
|
|
1141
|
+
async function resolveTarget(fs, dir, entry) {
|
|
1142
|
+
const configName = configNameFromLegacy(entry);
|
|
1143
|
+
const configPath = joinPath2(dir, configName);
|
|
1144
|
+
const dotConfigPath = joinPath2(dir, `.${configName}`);
|
|
1145
|
+
if (await existsPath(fs, dotConfigPath)) {
|
|
1146
|
+
return { action: "skip" };
|
|
1147
|
+
}
|
|
1148
|
+
if (await existsPath(fs, configPath)) {
|
|
1149
|
+
return { action: "rename", newPath: versionPathFor(configPath) ?? `${configName}.version` };
|
|
1150
|
+
}
|
|
1151
|
+
return { action: "delete" };
|
|
1152
|
+
}
|
|
1153
|
+
async function migrateVersionSidecars(fs, options = {}) {
|
|
1154
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
1155
|
+
const dryRun = options.dryRun ?? false;
|
|
1156
|
+
const replicas = options.replicas ?? [];
|
|
1157
|
+
const result = {
|
|
1158
|
+
renamed: [],
|
|
1159
|
+
deleted: [],
|
|
1160
|
+
remoteDeleted: [],
|
|
1161
|
+
failed: []
|
|
1162
|
+
};
|
|
1163
|
+
const stack = [root];
|
|
1164
|
+
while (stack.length > 0) {
|
|
1165
|
+
const dir = stack.pop();
|
|
1166
|
+
let entries;
|
|
1167
|
+
try {
|
|
1168
|
+
entries = await fs.readdir(dir);
|
|
1169
|
+
} catch {
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
for (const entry of entries) {
|
|
1173
|
+
const full = joinPath2(dir, entry);
|
|
1174
|
+
let stat;
|
|
1175
|
+
try {
|
|
1176
|
+
stat = await fs.stat(full);
|
|
1177
|
+
} catch {
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
if (isDir(stat)) {
|
|
1181
|
+
stack.push(full);
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
const isLegacyVersion = entry.startsWith(".") && (entry.endsWith(".version") || entry.endsWith(".version.mtime"));
|
|
1185
|
+
if (!isLegacyVersion) continue;
|
|
1186
|
+
const legacyVerPath = entry.endsWith(".version.mtime") ? full.slice(0, -".mtime".length) : full;
|
|
1187
|
+
const target = await resolveTarget(fs, dir, entry);
|
|
1188
|
+
if (target.action === "skip") continue;
|
|
1189
|
+
const legacyMtimePath = `${legacyVerPath}.mtime`;
|
|
1190
|
+
const newMtimePath = target.newPath ? `${target.newPath}.mtime` : null;
|
|
1191
|
+
if (dryRun) {
|
|
1192
|
+
if (target.action === "rename") result.renamed.push(full);
|
|
1193
|
+
else result.deleted.push(full);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
if (target.action === "rename") {
|
|
1198
|
+
if (await existsPath(fs, target.newPath)) {
|
|
1199
|
+
await fs.unlink(legacyVerPath);
|
|
1200
|
+
result.deleted.push(full);
|
|
1201
|
+
} else {
|
|
1202
|
+
const content = await fs.readFile(legacyVerPath);
|
|
1203
|
+
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
1204
|
+
await fs.writeFile(target.newPath, bytes);
|
|
1205
|
+
await fs.unlink(legacyVerPath);
|
|
1206
|
+
result.renamed.push(full);
|
|
1207
|
+
}
|
|
1208
|
+
if (newMtimePath && await existsPath(fs, legacyMtimePath)) {
|
|
1209
|
+
try {
|
|
1210
|
+
const mc = await fs.readFile(legacyMtimePath);
|
|
1211
|
+
const mb = typeof mc === "string" ? new TextEncoder().encode(mc) : mc;
|
|
1212
|
+
await fs.writeFile(newMtimePath, mb);
|
|
1213
|
+
await fs.unlink(legacyMtimePath);
|
|
1214
|
+
} catch {
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
} else {
|
|
1218
|
+
await fs.unlink(legacyVerPath);
|
|
1219
|
+
result.deleted.push(full);
|
|
1220
|
+
if (await existsPath(fs, legacyMtimePath)) {
|
|
1221
|
+
try {
|
|
1222
|
+
await fs.unlink(legacyMtimePath);
|
|
1223
|
+
} catch {
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
} catch (e) {
|
|
1228
|
+
result.failed.push(full);
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
for (const rep of replicas) {
|
|
1232
|
+
for (const rp of [legacyVerPath, legacyMtimePath]) {
|
|
1233
|
+
try {
|
|
1234
|
+
await rep.unlink(rp);
|
|
1235
|
+
result.remoteDeleted.push(rp);
|
|
1236
|
+
} catch {
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return result;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1097
1245
|
// src/config-repo.ts
|
|
1098
1246
|
var log2 = (0, import_localstorage_logger2.createLogger)("zen-fs-config:config-repo");
|
|
1099
1247
|
var META_DIR = "/.meta";
|
|
@@ -1576,6 +1724,42 @@ var ConfigRepo = class {
|
|
|
1576
1724
|
}
|
|
1577
1725
|
return result;
|
|
1578
1726
|
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
1729
|
+
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
1730
|
+
* and deletes the residual legacy copy from every replica backend so sync
|
|
1731
|
+
* does not pull it back. Best-effort: failures are logged, never thrown.
|
|
1732
|
+
*
|
|
1733
|
+
* Runs once at startup (see createConfigRepo). Safe to call again.
|
|
1734
|
+
*/
|
|
1735
|
+
async migrateLegacyVersionSidecars() {
|
|
1736
|
+
this.assertNotDisposed();
|
|
1737
|
+
const replicas = [...this.replicaBackends.values()].map((r) => ({
|
|
1738
|
+
unlink: (p) => r.syncable.unlink(p)
|
|
1739
|
+
}));
|
|
1740
|
+
const result = await migrateVersionSidecars(this.cachedFS, { replicas });
|
|
1741
|
+
if (result.renamed.length > 0) {
|
|
1742
|
+
log2.log(
|
|
1743
|
+
`[ConfigRepo] migrated ${result.renamed.length} legacy version sidecar(s) to new naming`,
|
|
1744
|
+
result.renamed
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
if (result.deleted.length > 0) {
|
|
1748
|
+
log2.log(
|
|
1749
|
+
`[ConfigRepo] removed ${result.deleted.length} orphaned legacy version sidecar(s)`,
|
|
1750
|
+
result.deleted
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
if (result.remoteDeleted.length > 0) {
|
|
1754
|
+
log2.log(
|
|
1755
|
+
`[ConfigRepo] removed ${result.remoteDeleted.length} remote residual legacy version sidecar(s)`
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
if (result.failed.length > 0) {
|
|
1759
|
+
log2.warn(`[ConfigRepo] failed to migrate ${result.failed.length} legacy version sidecar(s):`, result.failed);
|
|
1760
|
+
}
|
|
1761
|
+
return result;
|
|
1762
|
+
}
|
|
1579
1763
|
/**
|
|
1580
1764
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
1581
1765
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -1981,7 +2165,9 @@ var ConfigRepo = class {
|
|
|
1981
2165
|
try {
|
|
1982
2166
|
const entries = await this.cachedFS.readdir(current);
|
|
1983
2167
|
const statResults = await Promise.all(
|
|
1984
|
-
entries.filter(
|
|
2168
|
+
entries.filter(
|
|
2169
|
+
(entry) => !entry.startsWith(".") && !entry.endsWith(".version") && !entry.endsWith(".mtime")
|
|
2170
|
+
).map(async (entry) => {
|
|
1985
2171
|
const fullPath = current === "/" ? `/${entry}` : `${current}/${entry}`;
|
|
1986
2172
|
try {
|
|
1987
2173
|
const stat = await this.cachedFS.stat(fullPath);
|
|
@@ -2747,6 +2933,16 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
2747
2933
|
cacheOptions
|
|
2748
2934
|
);
|
|
2749
2935
|
await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
|
|
2936
|
+
if (options.migrateVersionSidecars !== false) {
|
|
2937
|
+
try {
|
|
2938
|
+
const mig = await repo.migrateLegacyVersionSidecars();
|
|
2939
|
+
if (mig.renamed.length === 0 && mig.deleted.length === 0 && mig.remoteDeleted.length === 0) {
|
|
2940
|
+
log2.log("[createConfigRepo] no legacy version sidecars to migrate");
|
|
2941
|
+
}
|
|
2942
|
+
} catch (err) {
|
|
2943
|
+
log2.warn(`[createConfigRepo] version sidecar migration failed:`, err?.message ?? err);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2750
2946
|
await repo.load();
|
|
2751
2947
|
if (repo.replicaCount > 0) {
|
|
2752
2948
|
log2.log("[createConfigRepo] Starting background initial sync + dedup...");
|
|
@@ -2882,10 +3078,12 @@ init_backend_registry();
|
|
|
2882
3078
|
incrementVersion,
|
|
2883
3079
|
isBrowserEnv,
|
|
2884
3080
|
isMtimeSidecar,
|
|
3081
|
+
legacyVersionPathFor,
|
|
2885
3082
|
listBackendMetadata,
|
|
2886
3083
|
listBackends,
|
|
2887
3084
|
localPrimaryType,
|
|
2888
3085
|
mergeAccountFields,
|
|
3086
|
+
migrateVersionSidecars,
|
|
2889
3087
|
purgeMtimeSidecars,
|
|
2890
3088
|
readVersion,
|
|
2891
3089
|
registerBackend,
|
package/dist/index.mjs
CHANGED
|
@@ -401,9 +401,30 @@ function versionPathFor(configFilePath) {
|
|
|
401
401
|
if (fileName.endsWith(".version")) {
|
|
402
402
|
return null;
|
|
403
403
|
}
|
|
404
|
-
const versionFileName =
|
|
404
|
+
const versionFileName = `${fileName}.version`;
|
|
405
405
|
return dir ? `${dir}/${versionFileName}` : versionFileName;
|
|
406
406
|
}
|
|
407
|
+
function legacyVersionPathFor(configFilePath) {
|
|
408
|
+
const lastSlash = configFilePath.lastIndexOf("/");
|
|
409
|
+
const dir = lastSlash >= 0 ? configFilePath.slice(0, lastSlash) : "";
|
|
410
|
+
const fileName = lastSlash >= 0 ? configFilePath.slice(lastSlash + 1) : configFilePath;
|
|
411
|
+
if (fileName.endsWith(".version")) return null;
|
|
412
|
+
if (fileName.startsWith(".")) return null;
|
|
413
|
+
const legacyFileName = `.${fileName}.version`;
|
|
414
|
+
return dir ? `${dir}/${legacyFileName}` : legacyFileName;
|
|
415
|
+
}
|
|
416
|
+
async function readVersionForConfig(fs, configFilePath) {
|
|
417
|
+
const newPath = versionPathFor(configFilePath);
|
|
418
|
+
if (newPath) {
|
|
419
|
+
const v = await readVersion(fs, newPath);
|
|
420
|
+
if (v) return v;
|
|
421
|
+
}
|
|
422
|
+
const legacyPath = legacyVersionPathFor(configFilePath);
|
|
423
|
+
if (legacyPath) {
|
|
424
|
+
return readVersion(fs, legacyPath);
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
407
428
|
function sha256PureJS(data) {
|
|
408
429
|
const K = new Uint32Array([
|
|
409
430
|
1116352408,
|
|
@@ -567,7 +588,7 @@ async function writeVersion(fs, versionFilePath, meta) {
|
|
|
567
588
|
}
|
|
568
589
|
async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
569
590
|
const vPath = versionPathFor(configFilePath);
|
|
570
|
-
const prev = vPath ? await
|
|
591
|
+
const prev = vPath ? await readVersionForConfig(fs, configFilePath) : null;
|
|
571
592
|
const hash = await sha256(newContent);
|
|
572
593
|
if (prev && prev.hash === hash) {
|
|
573
594
|
return prev;
|
|
@@ -582,7 +603,7 @@ async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
|
582
603
|
async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
583
604
|
const vPath = versionPathFor(configFilePath);
|
|
584
605
|
if (!vPath) return null;
|
|
585
|
-
const existing = await
|
|
606
|
+
const existing = await readVersionForConfig(fs, configFilePath);
|
|
586
607
|
if (!existing) return null;
|
|
587
608
|
try {
|
|
588
609
|
const content = await fs.readFile(configFilePath);
|
|
@@ -667,6 +688,131 @@ async function purgeMtimeSidecars(fs, options = {}) {
|
|
|
667
688
|
return result;
|
|
668
689
|
}
|
|
669
690
|
|
|
691
|
+
// src/version-migration.ts
|
|
692
|
+
var S_IFDIR2 = 16384;
|
|
693
|
+
function isDir(stat) {
|
|
694
|
+
return typeof stat.mode === "number" && (stat.mode & S_IFDIR2) === S_IFDIR2;
|
|
695
|
+
}
|
|
696
|
+
function joinPath2(dir, entry) {
|
|
697
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
698
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
699
|
+
}
|
|
700
|
+
async function existsPath(fs, p) {
|
|
701
|
+
try {
|
|
702
|
+
await fs.stat(p);
|
|
703
|
+
return true;
|
|
704
|
+
} catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function configNameFromLegacy(entry) {
|
|
709
|
+
const noMtime = entry.endsWith(".version.mtime") ? entry.slice(0, -".version.mtime".length) : entry.slice(0, -".version".length);
|
|
710
|
+
return noMtime.slice(1);
|
|
711
|
+
}
|
|
712
|
+
async function resolveTarget(fs, dir, entry) {
|
|
713
|
+
const configName = configNameFromLegacy(entry);
|
|
714
|
+
const configPath = joinPath2(dir, configName);
|
|
715
|
+
const dotConfigPath = joinPath2(dir, `.${configName}`);
|
|
716
|
+
if (await existsPath(fs, dotConfigPath)) {
|
|
717
|
+
return { action: "skip" };
|
|
718
|
+
}
|
|
719
|
+
if (await existsPath(fs, configPath)) {
|
|
720
|
+
return { action: "rename", newPath: versionPathFor(configPath) ?? `${configName}.version` };
|
|
721
|
+
}
|
|
722
|
+
return { action: "delete" };
|
|
723
|
+
}
|
|
724
|
+
async function migrateVersionSidecars(fs, options = {}) {
|
|
725
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
726
|
+
const dryRun = options.dryRun ?? false;
|
|
727
|
+
const replicas = options.replicas ?? [];
|
|
728
|
+
const result = {
|
|
729
|
+
renamed: [],
|
|
730
|
+
deleted: [],
|
|
731
|
+
remoteDeleted: [],
|
|
732
|
+
failed: []
|
|
733
|
+
};
|
|
734
|
+
const stack = [root];
|
|
735
|
+
while (stack.length > 0) {
|
|
736
|
+
const dir = stack.pop();
|
|
737
|
+
let entries;
|
|
738
|
+
try {
|
|
739
|
+
entries = await fs.readdir(dir);
|
|
740
|
+
} catch {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
for (const entry of entries) {
|
|
744
|
+
const full = joinPath2(dir, entry);
|
|
745
|
+
let stat;
|
|
746
|
+
try {
|
|
747
|
+
stat = await fs.stat(full);
|
|
748
|
+
} catch {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (isDir(stat)) {
|
|
752
|
+
stack.push(full);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
const isLegacyVersion = entry.startsWith(".") && (entry.endsWith(".version") || entry.endsWith(".version.mtime"));
|
|
756
|
+
if (!isLegacyVersion) continue;
|
|
757
|
+
const legacyVerPath = entry.endsWith(".version.mtime") ? full.slice(0, -".mtime".length) : full;
|
|
758
|
+
const target = await resolveTarget(fs, dir, entry);
|
|
759
|
+
if (target.action === "skip") continue;
|
|
760
|
+
const legacyMtimePath = `${legacyVerPath}.mtime`;
|
|
761
|
+
const newMtimePath = target.newPath ? `${target.newPath}.mtime` : null;
|
|
762
|
+
if (dryRun) {
|
|
763
|
+
if (target.action === "rename") result.renamed.push(full);
|
|
764
|
+
else result.deleted.push(full);
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
if (target.action === "rename") {
|
|
769
|
+
if (await existsPath(fs, target.newPath)) {
|
|
770
|
+
await fs.unlink(legacyVerPath);
|
|
771
|
+
result.deleted.push(full);
|
|
772
|
+
} else {
|
|
773
|
+
const content = await fs.readFile(legacyVerPath);
|
|
774
|
+
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
775
|
+
await fs.writeFile(target.newPath, bytes);
|
|
776
|
+
await fs.unlink(legacyVerPath);
|
|
777
|
+
result.renamed.push(full);
|
|
778
|
+
}
|
|
779
|
+
if (newMtimePath && await existsPath(fs, legacyMtimePath)) {
|
|
780
|
+
try {
|
|
781
|
+
const mc = await fs.readFile(legacyMtimePath);
|
|
782
|
+
const mb = typeof mc === "string" ? new TextEncoder().encode(mc) : mc;
|
|
783
|
+
await fs.writeFile(newMtimePath, mb);
|
|
784
|
+
await fs.unlink(legacyMtimePath);
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
} else {
|
|
789
|
+
await fs.unlink(legacyVerPath);
|
|
790
|
+
result.deleted.push(full);
|
|
791
|
+
if (await existsPath(fs, legacyMtimePath)) {
|
|
792
|
+
try {
|
|
793
|
+
await fs.unlink(legacyMtimePath);
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch (e) {
|
|
799
|
+
result.failed.push(full);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
for (const rep of replicas) {
|
|
803
|
+
for (const rp of [legacyVerPath, legacyMtimePath]) {
|
|
804
|
+
try {
|
|
805
|
+
await rep.unlink(rp);
|
|
806
|
+
result.remoteDeleted.push(rp);
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
815
|
+
|
|
670
816
|
// src/config-repo.ts
|
|
671
817
|
var log2 = createLogger2("zen-fs-config:config-repo");
|
|
672
818
|
var META_DIR = "/.meta";
|
|
@@ -1149,6 +1295,42 @@ var ConfigRepo = class {
|
|
|
1149
1295
|
}
|
|
1150
1296
|
return result;
|
|
1151
1297
|
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
1300
|
+
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
1301
|
+
* and deletes the residual legacy copy from every replica backend so sync
|
|
1302
|
+
* does not pull it back. Best-effort: failures are logged, never thrown.
|
|
1303
|
+
*
|
|
1304
|
+
* Runs once at startup (see createConfigRepo). Safe to call again.
|
|
1305
|
+
*/
|
|
1306
|
+
async migrateLegacyVersionSidecars() {
|
|
1307
|
+
this.assertNotDisposed();
|
|
1308
|
+
const replicas = [...this.replicaBackends.values()].map((r) => ({
|
|
1309
|
+
unlink: (p) => r.syncable.unlink(p)
|
|
1310
|
+
}));
|
|
1311
|
+
const result = await migrateVersionSidecars(this.cachedFS, { replicas });
|
|
1312
|
+
if (result.renamed.length > 0) {
|
|
1313
|
+
log2.log(
|
|
1314
|
+
`[ConfigRepo] migrated ${result.renamed.length} legacy version sidecar(s) to new naming`,
|
|
1315
|
+
result.renamed
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
if (result.deleted.length > 0) {
|
|
1319
|
+
log2.log(
|
|
1320
|
+
`[ConfigRepo] removed ${result.deleted.length} orphaned legacy version sidecar(s)`,
|
|
1321
|
+
result.deleted
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
if (result.remoteDeleted.length > 0) {
|
|
1325
|
+
log2.log(
|
|
1326
|
+
`[ConfigRepo] removed ${result.remoteDeleted.length} remote residual legacy version sidecar(s)`
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
if (result.failed.length > 0) {
|
|
1330
|
+
log2.warn(`[ConfigRepo] failed to migrate ${result.failed.length} legacy version sidecar(s):`, result.failed);
|
|
1331
|
+
}
|
|
1332
|
+
return result;
|
|
1333
|
+
}
|
|
1152
1334
|
/**
|
|
1153
1335
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
1154
1336
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -1554,7 +1736,9 @@ var ConfigRepo = class {
|
|
|
1554
1736
|
try {
|
|
1555
1737
|
const entries = await this.cachedFS.readdir(current);
|
|
1556
1738
|
const statResults = await Promise.all(
|
|
1557
|
-
entries.filter(
|
|
1739
|
+
entries.filter(
|
|
1740
|
+
(entry) => !entry.startsWith(".") && !entry.endsWith(".version") && !entry.endsWith(".mtime")
|
|
1741
|
+
).map(async (entry) => {
|
|
1558
1742
|
const fullPath = current === "/" ? `/${entry}` : `${current}/${entry}`;
|
|
1559
1743
|
try {
|
|
1560
1744
|
const stat = await this.cachedFS.stat(fullPath);
|
|
@@ -2320,6 +2504,16 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
2320
2504
|
cacheOptions
|
|
2321
2505
|
);
|
|
2322
2506
|
await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
|
|
2507
|
+
if (options.migrateVersionSidecars !== false) {
|
|
2508
|
+
try {
|
|
2509
|
+
const mig = await repo.migrateLegacyVersionSidecars();
|
|
2510
|
+
if (mig.renamed.length === 0 && mig.deleted.length === 0 && mig.remoteDeleted.length === 0) {
|
|
2511
|
+
log2.log("[createConfigRepo] no legacy version sidecars to migrate");
|
|
2512
|
+
}
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
log2.warn(`[createConfigRepo] version sidecar migration failed:`, err?.message ?? err);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2323
2517
|
await repo.load();
|
|
2324
2518
|
if (repo.replicaCount > 0) {
|
|
2325
2519
|
log2.log("[createConfigRepo] Starting background initial sync + dedup...");
|
|
@@ -2450,10 +2644,12 @@ export {
|
|
|
2450
2644
|
incrementVersion,
|
|
2451
2645
|
isBrowserEnv,
|
|
2452
2646
|
isMtimeSidecar,
|
|
2647
|
+
legacyVersionPathFor,
|
|
2453
2648
|
listBackendMetadata,
|
|
2454
2649
|
listBackends,
|
|
2455
2650
|
localPrimaryType,
|
|
2456
2651
|
mergeAccountFields,
|
|
2652
|
+
migrateVersionSidecars,
|
|
2457
2653
|
purgeMtimeSidecars,
|
|
2458
2654
|
readVersion,
|
|
2459
2655
|
registerBackend,
|
package/dist/zen-fs-config.js
CHANGED
|
@@ -5275,7 +5275,7 @@ var ZenFSConfig = (() => {
|
|
|
5275
5275
|
* @param path The path to remove from the file system.
|
|
5276
5276
|
* @param isDir Does the path belong to a directory, or a file?
|
|
5277
5277
|
*/
|
|
5278
|
-
async remove(path,
|
|
5278
|
+
async remove(path, isDir2) {
|
|
5279
5279
|
const env_25 = { stack: [], error: void 0, hasError: false };
|
|
5280
5280
|
try {
|
|
5281
5281
|
const tx = __addDisposableResource(env_25, this.transaction(), true);
|
|
@@ -5285,7 +5285,7 @@ var ZenFSConfig = (() => {
|
|
|
5285
5285
|
const ino = listing[fileName];
|
|
5286
5286
|
const inode = new Inode(await tx.get(ino) ?? _throw(withErrno("ENOENT")));
|
|
5287
5287
|
delete listing[fileName];
|
|
5288
|
-
if (!
|
|
5288
|
+
if (!isDir2 && isDirectory2(inode))
|
|
5289
5289
|
throw withErrno("EISDIR");
|
|
5290
5290
|
await tx.set(parentNode.data, encodeDirListing(listing));
|
|
5291
5291
|
if (inode.nlink > 1) {
|
|
@@ -5311,7 +5311,7 @@ var ZenFSConfig = (() => {
|
|
|
5311
5311
|
* @param path The path to remove from the file system.
|
|
5312
5312
|
* @param isDir Does the path belong to a directory, or a file?
|
|
5313
5313
|
*/
|
|
5314
|
-
removeSync(path,
|
|
5314
|
+
removeSync(path, isDir2) {
|
|
5315
5315
|
const env_26 = { stack: [], error: void 0, hasError: false };
|
|
5316
5316
|
try {
|
|
5317
5317
|
const tx = __addDisposableResource(env_26, this.transaction(), false);
|
|
@@ -5320,7 +5320,7 @@ var ZenFSConfig = (() => {
|
|
|
5320
5320
|
throw withErrno("ENOENT");
|
|
5321
5321
|
const inode = new Inode(tx.getSync(ino) ?? _throw(withErrno("ENOENT")));
|
|
5322
5322
|
delete listing[fileName];
|
|
5323
|
-
if (!
|
|
5323
|
+
if (!isDir2 && isDirectory2(inode))
|
|
5324
5324
|
throw withErrno("EISDIR");
|
|
5325
5325
|
tx.setSync(parentNode.data, encodeDirListing(listing));
|
|
5326
5326
|
if (inode.nlink > 1) {
|
|
@@ -19499,12 +19499,12 @@ var ZenFSConfig = (() => {
|
|
|
19499
19499
|
const inode = this.index.get(path);
|
|
19500
19500
|
if (!inode)
|
|
19501
19501
|
throw withErrno("ENOENT");
|
|
19502
|
-
const
|
|
19503
|
-
if (!
|
|
19502
|
+
const isDir2 = (inode.mode & S_IFMT) == S_IFDIR;
|
|
19503
|
+
if (!isDir2 && !isUnlink)
|
|
19504
19504
|
throw withErrno("ENOTDIR");
|
|
19505
|
-
if (
|
|
19505
|
+
if (isDir2 && isUnlink)
|
|
19506
19506
|
throw withErrno("EISDIR");
|
|
19507
|
-
if (!
|
|
19507
|
+
if (!isDir2)
|
|
19508
19508
|
this.index.delete(path);
|
|
19509
19509
|
}
|
|
19510
19510
|
async unlink(path) {
|
|
@@ -23805,17 +23805,17 @@ var ZenFSConfig = (() => {
|
|
|
23805
23805
|
const inode = this.index.get(path);
|
|
23806
23806
|
if (!inode)
|
|
23807
23807
|
throw withErrno("ENOENT");
|
|
23808
|
-
const
|
|
23808
|
+
const isDir2 = (inode.mode & constants_exports.S_IFMT) == constants_exports.S_IFDIR;
|
|
23809
23809
|
let handle;
|
|
23810
23810
|
try {
|
|
23811
|
-
handle = await this.get(
|
|
23811
|
+
handle = await this.get(isDir2 ? "directory" : "file", path);
|
|
23812
23812
|
} catch {
|
|
23813
23813
|
const parent = await this.get("directory", dirname(path));
|
|
23814
|
-
handle = await parent[
|
|
23814
|
+
handle = await parent[isDir2 ? "getDirectoryHandle" : "getFileHandle"](basename(path), { create: true }).catch((ex) => _throw(convertException(ex, path)));
|
|
23815
23815
|
if (!this.disableHandleCache)
|
|
23816
23816
|
this._handles.set(path, handle);
|
|
23817
23817
|
}
|
|
23818
|
-
if (
|
|
23818
|
+
if (isDir2)
|
|
23819
23819
|
return;
|
|
23820
23820
|
if (isKind(handle, "directory")) {
|
|
23821
23821
|
log_exports.crit(withErrno("EIO", "Mismatch in entry kind on write"));
|
|
@@ -25112,10 +25112,12 @@ var ZenFSConfig = (() => {
|
|
|
25112
25112
|
incrementVersion: () => incrementVersion,
|
|
25113
25113
|
isBrowserEnv: () => isBrowserEnv,
|
|
25114
25114
|
isMtimeSidecar: () => isMtimeSidecar,
|
|
25115
|
+
legacyVersionPathFor: () => legacyVersionPathFor,
|
|
25115
25116
|
listBackendMetadata: () => listBackendMetadata,
|
|
25116
25117
|
listBackends: () => listBackends,
|
|
25117
25118
|
localPrimaryType: () => localPrimaryType,
|
|
25118
25119
|
mergeAccountFields: () => mergeAccountFields,
|
|
25120
|
+
migrateVersionSidecars: () => migrateVersionSidecars,
|
|
25119
25121
|
purgeMtimeSidecars: () => purgeMtimeSidecars,
|
|
25120
25122
|
readVersion: () => readVersion,
|
|
25121
25123
|
registerBackend: () => registerBackend,
|
|
@@ -26705,9 +26707,30 @@ var ZenFSConfig = (() => {
|
|
|
26705
26707
|
if (fileName.endsWith(".version")) {
|
|
26706
26708
|
return null;
|
|
26707
26709
|
}
|
|
26708
|
-
const versionFileName =
|
|
26710
|
+
const versionFileName = `${fileName}.version`;
|
|
26709
26711
|
return dir ? `${dir}/${versionFileName}` : versionFileName;
|
|
26710
26712
|
}
|
|
26713
|
+
function legacyVersionPathFor(configFilePath) {
|
|
26714
|
+
const lastSlash = configFilePath.lastIndexOf("/");
|
|
26715
|
+
const dir = lastSlash >= 0 ? configFilePath.slice(0, lastSlash) : "";
|
|
26716
|
+
const fileName = lastSlash >= 0 ? configFilePath.slice(lastSlash + 1) : configFilePath;
|
|
26717
|
+
if (fileName.endsWith(".version")) return null;
|
|
26718
|
+
if (fileName.startsWith(".")) return null;
|
|
26719
|
+
const legacyFileName = `.${fileName}.version`;
|
|
26720
|
+
return dir ? `${dir}/${legacyFileName}` : legacyFileName;
|
|
26721
|
+
}
|
|
26722
|
+
async function readVersionForConfig(fs, configFilePath) {
|
|
26723
|
+
const newPath = versionPathFor(configFilePath);
|
|
26724
|
+
if (newPath) {
|
|
26725
|
+
const v = await readVersion(fs, newPath);
|
|
26726
|
+
if (v) return v;
|
|
26727
|
+
}
|
|
26728
|
+
const legacyPath = legacyVersionPathFor(configFilePath);
|
|
26729
|
+
if (legacyPath) {
|
|
26730
|
+
return readVersion(fs, legacyPath);
|
|
26731
|
+
}
|
|
26732
|
+
return null;
|
|
26733
|
+
}
|
|
26711
26734
|
function sha256PureJS(data) {
|
|
26712
26735
|
const K = new Uint32Array([
|
|
26713
26736
|
1116352408,
|
|
@@ -26871,7 +26894,7 @@ var ZenFSConfig = (() => {
|
|
|
26871
26894
|
}
|
|
26872
26895
|
async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
26873
26896
|
const vPath = versionPathFor(configFilePath);
|
|
26874
|
-
const prev = vPath ? await
|
|
26897
|
+
const prev = vPath ? await readVersionForConfig(fs, configFilePath) : null;
|
|
26875
26898
|
const hash = await sha256(newContent);
|
|
26876
26899
|
if (prev && prev.hash === hash) {
|
|
26877
26900
|
return prev;
|
|
@@ -26886,7 +26909,7 @@ var ZenFSConfig = (() => {
|
|
|
26886
26909
|
async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
26887
26910
|
const vPath = versionPathFor(configFilePath);
|
|
26888
26911
|
if (!vPath) return null;
|
|
26889
|
-
const existing = await
|
|
26912
|
+
const existing = await readVersionForConfig(fs, configFilePath);
|
|
26890
26913
|
if (!existing) return null;
|
|
26891
26914
|
try {
|
|
26892
26915
|
const content = await fs.readFile(configFilePath);
|
|
@@ -26971,6 +26994,131 @@ var ZenFSConfig = (() => {
|
|
|
26971
26994
|
return result;
|
|
26972
26995
|
}
|
|
26973
26996
|
|
|
26997
|
+
// src/version-migration.ts
|
|
26998
|
+
var S_IFDIR3 = 16384;
|
|
26999
|
+
function isDir(stat5) {
|
|
27000
|
+
return typeof stat5.mode === "number" && (stat5.mode & S_IFDIR3) === S_IFDIR3;
|
|
27001
|
+
}
|
|
27002
|
+
function joinPath2(dir, entry) {
|
|
27003
|
+
if (dir === "/" || dir === "") return `/${entry}`;
|
|
27004
|
+
return `${dir.replace(/\/+$/, "")}/${entry}`;
|
|
27005
|
+
}
|
|
27006
|
+
async function existsPath(fs, p) {
|
|
27007
|
+
try {
|
|
27008
|
+
await fs.stat(p);
|
|
27009
|
+
return true;
|
|
27010
|
+
} catch {
|
|
27011
|
+
return false;
|
|
27012
|
+
}
|
|
27013
|
+
}
|
|
27014
|
+
function configNameFromLegacy(entry) {
|
|
27015
|
+
const noMtime = entry.endsWith(".version.mtime") ? entry.slice(0, -".version.mtime".length) : entry.slice(0, -".version".length);
|
|
27016
|
+
return noMtime.slice(1);
|
|
27017
|
+
}
|
|
27018
|
+
async function resolveTarget(fs, dir, entry) {
|
|
27019
|
+
const configName = configNameFromLegacy(entry);
|
|
27020
|
+
const configPath = joinPath2(dir, configName);
|
|
27021
|
+
const dotConfigPath = joinPath2(dir, `.${configName}`);
|
|
27022
|
+
if (await existsPath(fs, dotConfigPath)) {
|
|
27023
|
+
return { action: "skip" };
|
|
27024
|
+
}
|
|
27025
|
+
if (await existsPath(fs, configPath)) {
|
|
27026
|
+
return { action: "rename", newPath: versionPathFor(configPath) ?? `${configName}.version` };
|
|
27027
|
+
}
|
|
27028
|
+
return { action: "delete" };
|
|
27029
|
+
}
|
|
27030
|
+
async function migrateVersionSidecars(fs, options = {}) {
|
|
27031
|
+
const root = options.root && options.root !== "" ? options.root : "/";
|
|
27032
|
+
const dryRun = options.dryRun ?? false;
|
|
27033
|
+
const replicas = options.replicas ?? [];
|
|
27034
|
+
const result = {
|
|
27035
|
+
renamed: [],
|
|
27036
|
+
deleted: [],
|
|
27037
|
+
remoteDeleted: [],
|
|
27038
|
+
failed: []
|
|
27039
|
+
};
|
|
27040
|
+
const stack = [root];
|
|
27041
|
+
while (stack.length > 0) {
|
|
27042
|
+
const dir = stack.pop();
|
|
27043
|
+
let entries2;
|
|
27044
|
+
try {
|
|
27045
|
+
entries2 = await fs.readdir(dir);
|
|
27046
|
+
} catch {
|
|
27047
|
+
continue;
|
|
27048
|
+
}
|
|
27049
|
+
for (const entry of entries2) {
|
|
27050
|
+
const full = joinPath2(dir, entry);
|
|
27051
|
+
let stat5;
|
|
27052
|
+
try {
|
|
27053
|
+
stat5 = await fs.stat(full);
|
|
27054
|
+
} catch {
|
|
27055
|
+
continue;
|
|
27056
|
+
}
|
|
27057
|
+
if (isDir(stat5)) {
|
|
27058
|
+
stack.push(full);
|
|
27059
|
+
continue;
|
|
27060
|
+
}
|
|
27061
|
+
const isLegacyVersion = entry.startsWith(".") && (entry.endsWith(".version") || entry.endsWith(".version.mtime"));
|
|
27062
|
+
if (!isLegacyVersion) continue;
|
|
27063
|
+
const legacyVerPath = entry.endsWith(".version.mtime") ? full.slice(0, -".mtime".length) : full;
|
|
27064
|
+
const target = await resolveTarget(fs, dir, entry);
|
|
27065
|
+
if (target.action === "skip") continue;
|
|
27066
|
+
const legacyMtimePath = `${legacyVerPath}.mtime`;
|
|
27067
|
+
const newMtimePath = target.newPath ? `${target.newPath}.mtime` : null;
|
|
27068
|
+
if (dryRun) {
|
|
27069
|
+
if (target.action === "rename") result.renamed.push(full);
|
|
27070
|
+
else result.deleted.push(full);
|
|
27071
|
+
continue;
|
|
27072
|
+
}
|
|
27073
|
+
try {
|
|
27074
|
+
if (target.action === "rename") {
|
|
27075
|
+
if (await existsPath(fs, target.newPath)) {
|
|
27076
|
+
await fs.unlink(legacyVerPath);
|
|
27077
|
+
result.deleted.push(full);
|
|
27078
|
+
} else {
|
|
27079
|
+
const content = await fs.readFile(legacyVerPath);
|
|
27080
|
+
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
27081
|
+
await fs.writeFile(target.newPath, bytes);
|
|
27082
|
+
await fs.unlink(legacyVerPath);
|
|
27083
|
+
result.renamed.push(full);
|
|
27084
|
+
}
|
|
27085
|
+
if (newMtimePath && await existsPath(fs, legacyMtimePath)) {
|
|
27086
|
+
try {
|
|
27087
|
+
const mc = await fs.readFile(legacyMtimePath);
|
|
27088
|
+
const mb = typeof mc === "string" ? new TextEncoder().encode(mc) : mc;
|
|
27089
|
+
await fs.writeFile(newMtimePath, mb);
|
|
27090
|
+
await fs.unlink(legacyMtimePath);
|
|
27091
|
+
} catch {
|
|
27092
|
+
}
|
|
27093
|
+
}
|
|
27094
|
+
} else {
|
|
27095
|
+
await fs.unlink(legacyVerPath);
|
|
27096
|
+
result.deleted.push(full);
|
|
27097
|
+
if (await existsPath(fs, legacyMtimePath)) {
|
|
27098
|
+
try {
|
|
27099
|
+
await fs.unlink(legacyMtimePath);
|
|
27100
|
+
} catch {
|
|
27101
|
+
}
|
|
27102
|
+
}
|
|
27103
|
+
}
|
|
27104
|
+
} catch (e) {
|
|
27105
|
+
result.failed.push(full);
|
|
27106
|
+
continue;
|
|
27107
|
+
}
|
|
27108
|
+
for (const rep of replicas) {
|
|
27109
|
+
for (const rp of [legacyVerPath, legacyMtimePath]) {
|
|
27110
|
+
try {
|
|
27111
|
+
await rep.unlink(rp);
|
|
27112
|
+
result.remoteDeleted.push(rp);
|
|
27113
|
+
} catch {
|
|
27114
|
+
}
|
|
27115
|
+
}
|
|
27116
|
+
}
|
|
27117
|
+
}
|
|
27118
|
+
}
|
|
27119
|
+
return result;
|
|
27120
|
+
}
|
|
27121
|
+
|
|
26974
27122
|
// src/config-repo.ts
|
|
26975
27123
|
var log6 = createLogger2("zen-fs-config:config-repo");
|
|
26976
27124
|
var META_DIR = "/.meta";
|
|
@@ -27453,6 +27601,42 @@ var ZenFSConfig = (() => {
|
|
|
27453
27601
|
}
|
|
27454
27602
|
return result;
|
|
27455
27603
|
}
|
|
27604
|
+
/**
|
|
27605
|
+
* Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
|
|
27606
|
+
* `<name>.version` naming. Renames local sidecars (preserving content/history)
|
|
27607
|
+
* and deletes the residual legacy copy from every replica backend so sync
|
|
27608
|
+
* does not pull it back. Best-effort: failures are logged, never thrown.
|
|
27609
|
+
*
|
|
27610
|
+
* Runs once at startup (see createConfigRepo). Safe to call again.
|
|
27611
|
+
*/
|
|
27612
|
+
async migrateLegacyVersionSidecars() {
|
|
27613
|
+
this.assertNotDisposed();
|
|
27614
|
+
const replicas = [...this.replicaBackends.values()].map((r) => ({
|
|
27615
|
+
unlink: (p) => r.syncable.unlink(p)
|
|
27616
|
+
}));
|
|
27617
|
+
const result = await migrateVersionSidecars(this.cachedFS, { replicas });
|
|
27618
|
+
if (result.renamed.length > 0) {
|
|
27619
|
+
log6.log(
|
|
27620
|
+
`[ConfigRepo] migrated ${result.renamed.length} legacy version sidecar(s) to new naming`,
|
|
27621
|
+
result.renamed
|
|
27622
|
+
);
|
|
27623
|
+
}
|
|
27624
|
+
if (result.deleted.length > 0) {
|
|
27625
|
+
log6.log(
|
|
27626
|
+
`[ConfigRepo] removed ${result.deleted.length} orphaned legacy version sidecar(s)`,
|
|
27627
|
+
result.deleted
|
|
27628
|
+
);
|
|
27629
|
+
}
|
|
27630
|
+
if (result.remoteDeleted.length > 0) {
|
|
27631
|
+
log6.log(
|
|
27632
|
+
`[ConfigRepo] removed ${result.remoteDeleted.length} remote residual legacy version sidecar(s)`
|
|
27633
|
+
);
|
|
27634
|
+
}
|
|
27635
|
+
if (result.failed.length > 0) {
|
|
27636
|
+
log6.warn(`[ConfigRepo] failed to migrate ${result.failed.length} legacy version sidecar(s):`, result.failed);
|
|
27637
|
+
}
|
|
27638
|
+
return result;
|
|
27639
|
+
}
|
|
27456
27640
|
/**
|
|
27457
27641
|
* Perform a full sync + dedup cycle without the watch snapshot cache.
|
|
27458
27642
|
* Used by createConfigRepo to pull remote-only files (like duplicate
|
|
@@ -27858,7 +28042,9 @@ var ZenFSConfig = (() => {
|
|
|
27858
28042
|
try {
|
|
27859
28043
|
const entries2 = await this.cachedFS.readdir(current);
|
|
27860
28044
|
const statResults = await Promise.all(
|
|
27861
|
-
entries2.filter(
|
|
28045
|
+
entries2.filter(
|
|
28046
|
+
(entry) => !entry.startsWith(".") && !entry.endsWith(".version") && !entry.endsWith(".mtime")
|
|
28047
|
+
).map(async (entry) => {
|
|
27862
28048
|
const fullPath = current === "/" ? `/${entry}` : `${current}/${entry}`;
|
|
27863
28049
|
try {
|
|
27864
28050
|
const stat5 = await this.cachedFS.stat(fullPath);
|
|
@@ -28624,6 +28810,16 @@ var ZenFSConfig = (() => {
|
|
|
28624
28810
|
cacheOptions
|
|
28625
28811
|
);
|
|
28626
28812
|
await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
|
|
28813
|
+
if (options.migrateVersionSidecars !== false) {
|
|
28814
|
+
try {
|
|
28815
|
+
const mig = await repo.migrateLegacyVersionSidecars();
|
|
28816
|
+
if (mig.renamed.length === 0 && mig.deleted.length === 0 && mig.remoteDeleted.length === 0) {
|
|
28817
|
+
log6.log("[createConfigRepo] no legacy version sidecars to migrate");
|
|
28818
|
+
}
|
|
28819
|
+
} catch (err2) {
|
|
28820
|
+
log6.warn(`[createConfigRepo] version sidecar migration failed:`, err2?.message ?? err2);
|
|
28821
|
+
}
|
|
28822
|
+
}
|
|
28627
28823
|
await repo.load();
|
|
28628
28824
|
if (repo.replicaCount > 0) {
|
|
28629
28825
|
log6.log("[createConfigRepo] Starting background initial sync + dedup...");
|