@vrtmrz/livesync-commonlib 0.1.9 → 0.1.11
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/replication/couchdb/LiveSyncReplicator.d.ts +1 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +144 -115
- package/dist/serviceFeatures/offlineScanner.d.ts +19 -5
- package/dist/serviceFeatures/offlineScanner.js +61 -29
- package/dist/serviceModules/Rebuilder.js +9 -0
- package/docs/development.md +1 -1
- package/docs/proven-in-use.md +4 -0
- package/package.json +1 -1
|
@@ -32,6 +32,7 @@ export declare class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicato
|
|
|
32
32
|
updateMaxTransferredSeqOnChunks(localDB: PouchDB.Database, remoteID: number, seqStatusMap: Record<number, boolean>): Promise<ReturnType<typeof this.getEmptyMaxEntry>>;
|
|
33
33
|
sendChunks(setting: RemoteDBSettings, remoteDB: PouchDB.Database<EntryDoc> | undefined, showResult: boolean, fromSeq?: number | string): Promise<boolean>;
|
|
34
34
|
openOneShotReplication(setting: RemoteDBSettings, showResult: boolean, retrying: boolean, syncMode: "sync" | "pullOnly" | "pushOnly", ignoreCleanLock?: boolean): Promise<boolean>;
|
|
35
|
+
private closeRemoteDatabase;
|
|
35
36
|
replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise<boolean>;
|
|
36
37
|
replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise<boolean>;
|
|
37
38
|
checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean, showResult: boolean, ignoreCleanLock?: boolean): Promise<false | {
|
|
@@ -540,80 +540,94 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
540
540
|
);
|
|
541
541
|
return false;
|
|
542
542
|
}
|
|
543
|
-
this.maxPullSeq = Number(`${ret.info.update_seq}`.split("-")[0]);
|
|
544
|
-
this.maxPushSeq = Number(`${(await localDB.info()).update_seq}`.split("-")[0]);
|
|
545
|
-
if (showResult) {
|
|
546
|
-
Logger(this.translate("liveSyncReplicator.checkingLastSyncPoint"), LOG_LEVEL_NOTICE, "sync");
|
|
547
|
-
}
|
|
548
543
|
const { db, syncOptionBase } = ret;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
syncHandler,
|
|
563
|
-
showResult,
|
|
564
|
-
docSentOnStart,
|
|
565
|
-
docArrivedOnStart,
|
|
566
|
-
syncMode,
|
|
567
|
-
retrying,
|
|
568
|
-
false
|
|
569
|
-
);
|
|
570
|
-
if (syncResult == "DONE") {
|
|
571
|
-
return true;
|
|
572
|
-
}
|
|
573
|
-
if (syncResult == "CANCELLED") {
|
|
574
|
-
return false;
|
|
575
|
-
}
|
|
576
|
-
if (syncResult == "FAILED") {
|
|
577
|
-
return false;
|
|
578
|
-
}
|
|
579
|
-
if (syncResult == "NEED_RESURRECT") {
|
|
544
|
+
try {
|
|
545
|
+
this.maxPullSeq = Number(`${ret.info.update_seq}`.split("-")[0]);
|
|
546
|
+
this.maxPushSeq = Number(`${(await localDB.info()).update_seq}`.split("-")[0]);
|
|
547
|
+
if (showResult) {
|
|
548
|
+
Logger(this.translate("liveSyncReplicator.checkingLastSyncPoint"), LOG_LEVEL_NOTICE, "sync");
|
|
549
|
+
}
|
|
550
|
+
this.syncStatus = "STARTED";
|
|
551
|
+
this.updateInfo();
|
|
552
|
+
const docArrivedOnStart = this.docArrived;
|
|
553
|
+
const docSentOnStart = this.docSent;
|
|
554
|
+
if (!retrying) {
|
|
555
|
+
this.originalSetting = setting;
|
|
556
|
+
}
|
|
580
557
|
this.terminateSync();
|
|
581
|
-
|
|
582
|
-
|
|
558
|
+
const syncHandler = syncMode == "sync" ? localDB.sync(db, { ...syncOptionBase }) : syncMode == "pullOnly" ? localDB.replicate.from(db, {
|
|
559
|
+
...syncOptionBase,
|
|
560
|
+
...setting.readChunksOnline ? selectorOnDemandPull : {}
|
|
561
|
+
}) : syncMode == "pushOnly" ? localDB.replicate.to(db, { ...syncOptionBase }) : void 0;
|
|
562
|
+
const syncResult = await this.processSync(
|
|
563
|
+
syncHandler,
|
|
583
564
|
showResult,
|
|
584
|
-
|
|
565
|
+
docSentOnStart,
|
|
566
|
+
docArrivedOnStart,
|
|
585
567
|
syncMode,
|
|
586
|
-
|
|
568
|
+
retrying,
|
|
569
|
+
false
|
|
587
570
|
);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
tempSetting.batches_limit = Math.ceil(tempSetting.batches_limit / 2) + 2;
|
|
593
|
-
if (tempSetting.batch_size <= 5 && tempSetting.batches_limit <= 5) {
|
|
594
|
-
Logger(
|
|
595
|
-
this.translate("liveSyncReplicator.cantReplicateLowerValue"),
|
|
596
|
-
showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
|
|
597
|
-
);
|
|
571
|
+
if (syncResult == "DONE") {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
if (syncResult == "CANCELLED") {
|
|
598
575
|
return false;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
576
|
+
}
|
|
577
|
+
if (syncResult == "FAILED") {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
if (syncResult == "NEED_RESURRECT") {
|
|
581
|
+
this.terminateSync();
|
|
582
|
+
return async () => await this.openOneShotReplication(
|
|
583
|
+
this.originalSetting,
|
|
584
|
+
showResult,
|
|
585
|
+
false,
|
|
586
|
+
syncMode,
|
|
587
|
+
ignoreCleanLock
|
|
606
588
|
);
|
|
607
|
-
return async () => await this.openOneShotReplication(tempSetting, showResult, true, syncMode, ignoreCleanLock);
|
|
608
589
|
}
|
|
590
|
+
if (syncResult == "NEED_RETRY") {
|
|
591
|
+
const tempSetting = JSON.parse(JSON.stringify(setting));
|
|
592
|
+
tempSetting.batch_size = Math.ceil(tempSetting.batch_size / 2) + 2;
|
|
593
|
+
tempSetting.batches_limit = Math.ceil(tempSetting.batches_limit / 2) + 2;
|
|
594
|
+
if (tempSetting.batch_size <= 5 && tempSetting.batches_limit <= 5) {
|
|
595
|
+
Logger(
|
|
596
|
+
this.translate("liveSyncReplicator.cantReplicateLowerValue"),
|
|
597
|
+
showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
|
|
598
|
+
);
|
|
599
|
+
return false;
|
|
600
|
+
} else {
|
|
601
|
+
Logger(
|
|
602
|
+
this.translate("liveSyncReplicator.retryLowerBatchSize", {
|
|
603
|
+
batch_size: tempSetting.batch_size.toString(),
|
|
604
|
+
batches_limit: tempSetting.batches_limit.toString()
|
|
605
|
+
}),
|
|
606
|
+
showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
|
|
607
|
+
);
|
|
608
|
+
return async () => await this.openOneShotReplication(tempSetting, showResult, true, syncMode, ignoreCleanLock);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return false;
|
|
612
|
+
} finally {
|
|
613
|
+
await this.closeRemoteDatabase(db);
|
|
609
614
|
}
|
|
610
|
-
return false;
|
|
611
615
|
});
|
|
612
616
|
if (typeof next === "boolean") {
|
|
613
617
|
return next;
|
|
614
618
|
}
|
|
615
619
|
return await next();
|
|
616
620
|
}
|
|
621
|
+
// One-shot attempts own their temporary remote handle. Continuous replication
|
|
622
|
+
// keeps its handle until the replication controller terminates it.
|
|
623
|
+
async closeRemoteDatabase(db) {
|
|
624
|
+
try {
|
|
625
|
+
await db.close();
|
|
626
|
+
} catch (ex) {
|
|
627
|
+
Logger("Failed to close remote database.", LOG_LEVEL_INFO, "sync");
|
|
628
|
+
Logger(ex, LOG_LEVEL_VERBOSE, "sync");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
617
631
|
replicateAllToServer(setting, showingNotice) {
|
|
618
632
|
return this.openOneShotReplication(setting, showingNotice ?? false, false, "pushOnly");
|
|
619
633
|
}
|
|
@@ -638,72 +652,87 @@ ${sendingDocs}/(${sentDocsCount} done)`,
|
|
|
638
652
|
);
|
|
639
653
|
return false;
|
|
640
654
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
Logger(
|
|
668
|
-
"The remote database has been rebuilt or corrupted since we have synchronized last time. Fetch rebuilt DB, explicit unlocking or chunk clean-up is required.",
|
|
669
|
-
LOG_LEVEL_NOTICE
|
|
655
|
+
let ownershipTransferred = false;
|
|
656
|
+
try {
|
|
657
|
+
if (!skipCheck) {
|
|
658
|
+
if (!await checkRemoteVersion(dbRet.db, this.migrate.bind(this), VER)) {
|
|
659
|
+
Logger(this.translate("liveSyncReplicator.remoteDbCorrupted"), LOG_LEVEL_NOTICE);
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
this.remoteCleaned = false;
|
|
663
|
+
this.remoteLocked = false;
|
|
664
|
+
this.remoteLockedAndDeviceNotAccepted = false;
|
|
665
|
+
this.tweakSettingsMismatched = false;
|
|
666
|
+
this.preferredTweakValue = void 0;
|
|
667
|
+
const progress = `${dbRet.info.update_seq}`;
|
|
668
|
+
const info = {
|
|
669
|
+
app_version: this.env.services.API.getAppVersion(),
|
|
670
|
+
plugin_version: this.env.services.API.getPluginVersion(),
|
|
671
|
+
vault_name: this.env.services.vault.vaultName(),
|
|
672
|
+
device_name: this.env.services.vault.getVaultName(),
|
|
673
|
+
progress
|
|
674
|
+
};
|
|
675
|
+
const ensure = await ensureDatabaseIsCompatible(
|
|
676
|
+
dbRet.db,
|
|
677
|
+
setting,
|
|
678
|
+
this.nodeid,
|
|
679
|
+
currentVersionRange,
|
|
680
|
+
info
|
|
670
681
|
);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (
|
|
678
|
-
this.remoteLocked = true;
|
|
679
|
-
} else {
|
|
682
|
+
if (ensure == "INCOMPATIBLE") {
|
|
683
|
+
Logger(
|
|
684
|
+
"The remote database has no compatibility with the running version. Please upgrade the plugin.",
|
|
685
|
+
LOG_LEVEL_NOTICE
|
|
686
|
+
);
|
|
687
|
+
return false;
|
|
688
|
+
} else if (ensure == "NODE_LOCKED") {
|
|
680
689
|
Logger(
|
|
681
|
-
"The remote database has been
|
|
690
|
+
"The remote database has been rebuilt or corrupted since we have synchronized last time. Fetch rebuilt DB, explicit unlocking or chunk clean-up is required.",
|
|
682
691
|
LOG_LEVEL_NOTICE
|
|
683
692
|
);
|
|
684
693
|
this.remoteLockedAndDeviceNotAccepted = true;
|
|
685
694
|
this.remoteLocked = true;
|
|
686
|
-
|
|
695
|
+
return false;
|
|
696
|
+
} else if (ensure == "LOCKED") {
|
|
697
|
+
this.remoteLocked = true;
|
|
698
|
+
} else if (ensure == "NODE_CLEANED") {
|
|
699
|
+
if (ignoreCleanLock) {
|
|
700
|
+
this.remoteLocked = true;
|
|
701
|
+
} else {
|
|
702
|
+
Logger(
|
|
703
|
+
"The remote database has been cleaned up. Fetch rebuilt DB, explicit unlocking or chunk clean-up is required.",
|
|
704
|
+
LOG_LEVEL_NOTICE
|
|
705
|
+
);
|
|
706
|
+
this.remoteLockedAndDeviceNotAccepted = true;
|
|
707
|
+
this.remoteLocked = true;
|
|
708
|
+
this.remoteCleaned = true;
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
} else if (ensure == "OK") {
|
|
712
|
+
} else if (ensure[0] == "MISMATCHED") {
|
|
713
|
+
Logger(this.translate("liveSyncReplicator.mismatchedTweakDetected"), LOG_LEVEL_NOTICE);
|
|
714
|
+
this.tweakSettingsMismatched = true;
|
|
715
|
+
this.preferredTweakValue = ensure[1];
|
|
687
716
|
return false;
|
|
688
717
|
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
718
|
+
}
|
|
719
|
+
const syncOptionBase = {
|
|
720
|
+
batches_limit: setting.batches_limit,
|
|
721
|
+
batch_size: setting.batch_size
|
|
722
|
+
};
|
|
723
|
+
syncOptionBase.push = {};
|
|
724
|
+
if (setting.readChunksOnline) {
|
|
725
|
+
syncOptionBase.pull = { ...selectorOnDemandPull };
|
|
726
|
+
}
|
|
727
|
+
const syncOption = keepAlive ? { live: true, retry: true, heartbeat: setting.useTimeouts ? false : 3e4, ...syncOptionBase } : { ...syncOptionBase };
|
|
728
|
+
const connection = { db: dbRet.db, info: dbRet.info, syncOptionBase, syncOption };
|
|
729
|
+
ownershipTransferred = true;
|
|
730
|
+
return connection;
|
|
731
|
+
} finally {
|
|
732
|
+
if (!ownershipTransferred) {
|
|
733
|
+
await this.closeRemoteDatabase(dbRet.db);
|
|
695
734
|
}
|
|
696
735
|
}
|
|
697
|
-
const syncOptionBase = {
|
|
698
|
-
batches_limit: setting.batches_limit,
|
|
699
|
-
batch_size: setting.batch_size
|
|
700
|
-
};
|
|
701
|
-
syncOptionBase.push = {};
|
|
702
|
-
if (setting.readChunksOnline) {
|
|
703
|
-
syncOptionBase.pull = { ...selectorOnDemandPull };
|
|
704
|
-
}
|
|
705
|
-
const syncOption = keepAlive ? { live: true, retry: true, heartbeat: setting.useTimeouts ? false : 3e4, ...syncOptionBase } : { ...syncOptionBase };
|
|
706
|
-
return { db: dbRet.db, info: dbRet.info, syncOptionBase, syncOption };
|
|
707
736
|
}
|
|
708
737
|
async openContinuousReplication(setting, showResult, retrying) {
|
|
709
738
|
const next = await shareRunningResult("continuousReplication", async () => {
|
|
@@ -2,6 +2,19 @@ import { type FilePathWithPrefix, type FilePathWithPrefixLC, type MetaEntry, typ
|
|
|
2
2
|
import { type LogFunction } from "../services/lib/logUtils.js";
|
|
3
3
|
import type { NecessaryServices } from "../interfaces/ServiceModule.js";
|
|
4
4
|
import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
|
|
5
|
+
/**
|
|
6
|
+
* Outcome of processing one storage/database pair during an offline scan.
|
|
7
|
+
*
|
|
8
|
+
* A skipped pair is intentionally left unchanged by scanner policy. It is not
|
|
9
|
+
* a scan failure, but it must not receive success side effects which would
|
|
10
|
+
* claim that database content was reflected to storage.
|
|
11
|
+
*/
|
|
12
|
+
export declare const FilePairProcessResults: {
|
|
13
|
+
readonly COMPLETED: "completed";
|
|
14
|
+
readonly SKIPPED: "skipped";
|
|
15
|
+
readonly FAILED: "failed";
|
|
16
|
+
};
|
|
17
|
+
export type FilePairProcessResult = (typeof FilePairProcessResults)[keyof typeof FilePairProcessResults];
|
|
5
18
|
/**
|
|
6
19
|
* Collect deleted files that have expired according to retention policy.
|
|
7
20
|
* @param host Services container
|
|
@@ -23,7 +36,7 @@ export declare function getPathFromEntry(host: NecessaryServices<"path", never>,
|
|
|
23
36
|
* @param file Storage file information
|
|
24
37
|
* @param doc Database entry
|
|
25
38
|
*/
|
|
26
|
-
export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry): Promise<
|
|
39
|
+
export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry): Promise<FilePairProcessResult>;
|
|
27
40
|
export declare function canProceedScan(host: NecessaryServices<"keyValueDB" | "setting", never>, errorManager: UnresolvedErrorManager, log: LogFunction, showingNotice?: boolean, ignoreSuspending?: boolean): boolean;
|
|
28
41
|
/**
|
|
29
42
|
* Convert file path to lower case if the settings indicate that filename case should be handled insensitively.
|
|
@@ -46,9 +59,9 @@ export declare function collectDatabaseFiles(host: NecessaryServices<"database"
|
|
|
46
59
|
databaseFileNames: FilePathWithPrefix[];
|
|
47
60
|
databaseFileNameCI2CS: Record<FilePathWithPrefix, FilePathWithPrefixLC>;
|
|
48
61
|
}>;
|
|
49
|
-
export declare function updateToDatabase(host: NecessaryServices<"vault", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, file: UXFileInfoStub): Promise<
|
|
50
|
-
export declare function updateToStorage(host: NecessaryServices<"vault" | "path", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, w: MetaEntry): Promise<
|
|
51
|
-
export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry): Promise<
|
|
62
|
+
export declare function updateToDatabase(host: NecessaryServices<"vault", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, file: UXFileInfoStub): Promise<FilePairProcessResult>;
|
|
63
|
+
export declare function updateToStorage(host: NecessaryServices<"vault" | "path", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, w: MetaEntry): Promise<FilePairProcessResult>;
|
|
64
|
+
export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry): Promise<FilePairProcessResult>;
|
|
52
65
|
export declare const FullScanModes: {
|
|
53
66
|
readonly DB_APPLY: "db-apply";
|
|
54
67
|
readonly NEWER_WINS: "newer-wins";
|
|
@@ -105,6 +118,7 @@ export declare function resolveFilePairAction(state: FilePairState, options: Ful
|
|
|
105
118
|
* @param log Logging function
|
|
106
119
|
* @param errorManager Error manager
|
|
107
120
|
* @param options Full scan options
|
|
121
|
+
* @returns True when every pair completed or was deliberately skipped; false when any pair failed
|
|
108
122
|
*/
|
|
109
123
|
export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise<boolean>;
|
|
110
124
|
export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial<FullScanOptions> | boolean | undefined, ignoreSuspending?: boolean): FullScanOptions;
|
|
@@ -115,7 +129,7 @@ export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial
|
|
|
115
129
|
* @param errorManager Error manager
|
|
116
130
|
* @param showingNotice Whether to show notices during scanning
|
|
117
131
|
* @param ignoreSuspending Whether to ignore suspension settings
|
|
118
|
-
* @returns True
|
|
132
|
+
* @returns True when the scan was permitted and no selected pair failed
|
|
119
133
|
*/
|
|
120
134
|
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial<FullScanOptions>): Promise<boolean>;
|
|
121
135
|
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise<boolean>;
|
|
@@ -13,6 +13,11 @@ import { createInstanceLogFunction } from "../services/lib/logUtils.js";
|
|
|
13
13
|
import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../common/models/shared.const.symbols.js";
|
|
14
14
|
import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
|
|
15
15
|
import { compatGlobal } from "../common/coreEnvFunctions.js";
|
|
16
|
+
const FilePairProcessResults = {
|
|
17
|
+
COMPLETED: "completed",
|
|
18
|
+
SKIPPED: "skipped",
|
|
19
|
+
FAILED: "failed"
|
|
20
|
+
};
|
|
16
21
|
async function collectDeletedFiles(host, log) {
|
|
17
22
|
const limitDays = host.services.setting.currentSettings().automaticallyDeleteMetadataOfDeletedFiles;
|
|
18
23
|
if (limitDays <= 0) return;
|
|
@@ -59,13 +64,14 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
|
|
|
59
64
|
if (!host.services.vault.isFileSizeTooLarge(file.stat.size)) {
|
|
60
65
|
log("STORAGE -> DB :" + file.path);
|
|
61
66
|
await host.serviceModules.fileHandler.storeFileToDB(file);
|
|
67
|
+
return FilePairProcessResults.COMPLETED;
|
|
62
68
|
} else {
|
|
63
69
|
log(
|
|
64
70
|
`STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`,
|
|
65
71
|
LOG_LEVEL_NOTICE
|
|
66
72
|
);
|
|
73
|
+
return FilePairProcessResults.SKIPPED;
|
|
67
74
|
}
|
|
68
|
-
break;
|
|
69
75
|
case TARGET_IS_NEW:
|
|
70
76
|
if (!host.services.vault.isFileSizeTooLarge(doc.size)) {
|
|
71
77
|
log("STORAGE <- DB :" + docPath);
|
|
@@ -74,21 +80,24 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
|
|
|
74
80
|
file: file.path,
|
|
75
81
|
automated: true
|
|
76
82
|
});
|
|
83
|
+
return FilePairProcessResults.COMPLETED;
|
|
77
84
|
} else {
|
|
78
85
|
log(`STORAGE <- DB : Cloud not read ${file.path}, possibly deleted`, LOG_LEVEL_NOTICE);
|
|
86
|
+
return FilePairProcessResults.FAILED;
|
|
79
87
|
}
|
|
80
88
|
} else {
|
|
81
89
|
log(
|
|
82
90
|
`STORAGE <- DB : ${file.path} has been skipped due to file size exceeding the limit`,
|
|
83
91
|
LOG_LEVEL_NOTICE
|
|
84
92
|
);
|
|
93
|
+
return FilePairProcessResults.SKIPPED;
|
|
85
94
|
}
|
|
86
|
-
break;
|
|
87
95
|
case EVEN:
|
|
88
96
|
log("STORAGE == DB :" + file.path, LOG_LEVEL_DEBUG);
|
|
89
|
-
|
|
97
|
+
return FilePairProcessResults.COMPLETED;
|
|
90
98
|
default:
|
|
91
99
|
log("STORAGE ?? DB :" + file.path + " Something got weird");
|
|
100
|
+
return FilePairProcessResults.FAILED;
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
function canProceedScan(host, errorManager, log, showingNotice = false, ignoreSuspending = false) {
|
|
@@ -167,8 +176,10 @@ async function updateToDatabase(host, log, logLevel, file) {
|
|
|
167
176
|
const path = file.path;
|
|
168
177
|
await host.serviceModules.fileHandler.storeFileToDB(file);
|
|
169
178
|
host.services.context.events.emitEvent("event-file-changed", { file: path, automated: true });
|
|
179
|
+
return FilePairProcessResults.COMPLETED;
|
|
170
180
|
} else {
|
|
171
181
|
log(`UPDATE DATABASE: ${file.path} has been skipped due to file size exceeding the limit`, logLevel);
|
|
182
|
+
return FilePairProcessResults.SKIPPED;
|
|
172
183
|
}
|
|
173
184
|
}
|
|
174
185
|
async function updateToStorage(host, log, logLevel, w) {
|
|
@@ -177,35 +188,41 @@ async function updateToStorage(host, log, logLevel, w) {
|
|
|
177
188
|
if (!host.services.vault.isFileSizeTooLarge(w.size)) {
|
|
178
189
|
if ((w._conflicts?.length ?? 0) > 0) {
|
|
179
190
|
log(`UPDATE STORAGE: ${path} has conflicts. skipped (x)`, LOG_LEVEL_INFO);
|
|
180
|
-
return;
|
|
191
|
+
return FilePairProcessResults.SKIPPED;
|
|
181
192
|
}
|
|
182
|
-
await host.serviceModules.fileHandler.dbToStorage(path, null, true);
|
|
193
|
+
const reflected = await host.serviceModules.fileHandler.dbToStorage(path, null, true);
|
|
194
|
+
if (!reflected) return FilePairProcessResults.FAILED;
|
|
183
195
|
host.services.context.events.emitEvent("event-file-changed", {
|
|
184
196
|
file: path,
|
|
185
197
|
automated: true
|
|
186
198
|
});
|
|
187
199
|
log(`Check or pull from db:${path} OK`);
|
|
200
|
+
return FilePairProcessResults.COMPLETED;
|
|
188
201
|
} else {
|
|
189
202
|
log(`UPDATE STORAGE: ${path} has been skipped due to file size exceeding the limit`, logLevel);
|
|
203
|
+
return FilePairProcessResults.SKIPPED;
|
|
190
204
|
}
|
|
191
205
|
} else if (w) {
|
|
192
206
|
log(`Deletion history skipped: ${path}`, LOG_LEVEL_VERBOSE);
|
|
207
|
+
return FilePairProcessResults.SKIPPED;
|
|
193
208
|
} else {
|
|
194
209
|
log(`entry not found: ${path}`);
|
|
210
|
+
return FilePairProcessResults.FAILED;
|
|
195
211
|
}
|
|
196
212
|
}
|
|
197
213
|
async function syncStorageAndDatabase(host, log, file, logLevel, doc) {
|
|
198
214
|
if ((doc._conflicts?.length ?? 0) > 0) {
|
|
199
215
|
log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO);
|
|
200
|
-
return;
|
|
216
|
+
return FilePairProcessResults.SKIPPED;
|
|
201
217
|
}
|
|
202
218
|
if (!host.services.vault.isFileSizeTooLarge(file.stat.size) && !host.services.vault.isFileSizeTooLarge(doc.size)) {
|
|
203
|
-
await syncFileBetweenDBandStorage(host, log, file, doc);
|
|
219
|
+
return await syncFileBetweenDBandStorage(host, log, file, doc);
|
|
204
220
|
} else {
|
|
205
221
|
log(
|
|
206
222
|
`SYNC DATABASE AND STORAGE: ${getPathFromEntry(host, doc)} has been skipped due to file size exceeding the limit`,
|
|
207
223
|
logLevel
|
|
208
224
|
);
|
|
225
|
+
return FilePairProcessResults.SKIPPED;
|
|
209
226
|
}
|
|
210
227
|
}
|
|
211
228
|
const FullScanModes = {
|
|
@@ -308,7 +325,7 @@ async function processFilePair(host, log, pair, options) {
|
|
|
308
325
|
}
|
|
309
326
|
if (doc && (doc._conflicts?.length ?? 0) > 0) {
|
|
310
327
|
log(`SKIP ${options.mode}: ${path} has conflicts`, LOG_LEVEL_INFO);
|
|
311
|
-
return
|
|
328
|
+
return FilePairProcessResults.SKIPPED;
|
|
312
329
|
}
|
|
313
330
|
const state = getFilePairState(pair);
|
|
314
331
|
let action = resolveFilePairAction(state, options);
|
|
@@ -328,32 +345,35 @@ async function processFilePair(host, log, pair, options) {
|
|
|
328
345
|
if (!file) {
|
|
329
346
|
throw new Error(`Missing storage file for ${path}`);
|
|
330
347
|
}
|
|
331
|
-
await updateToDatabase(host, log, LOG_LEVEL_INFO, file);
|
|
332
|
-
return true;
|
|
348
|
+
return await updateToDatabase(host, log, LOG_LEVEL_INFO, file);
|
|
333
349
|
case "update-storage":
|
|
334
350
|
if (!doc) {
|
|
335
351
|
throw new Error(`Missing database entry for ${path}`);
|
|
336
352
|
}
|
|
337
|
-
await updateToStorage(host, log, LOG_LEVEL_INFO, doc);
|
|
338
|
-
|
|
339
|
-
|
|
353
|
+
const updateStorageResult = await updateToStorage(host, log, LOG_LEVEL_INFO, doc);
|
|
354
|
+
if (updateStorageResult === FilePairProcessResults.COMPLETED) {
|
|
355
|
+
updateFileMTimeInMap(host, fileMapKey, doc.mtime);
|
|
356
|
+
}
|
|
357
|
+
return updateStorageResult;
|
|
340
358
|
case "sync-newer":
|
|
341
359
|
if (!file || !doc) {
|
|
342
360
|
throw new Error(`Cannot compare freshness for ${path}`);
|
|
343
361
|
}
|
|
344
|
-
await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc);
|
|
345
|
-
|
|
346
|
-
|
|
362
|
+
const syncResult = await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc);
|
|
363
|
+
if (syncResult === FilePairProcessResults.COMPLETED) {
|
|
364
|
+
updateFileMTimeInMap(host, fileMapKey, Math.max(file.stat.mtime, doc.mtime));
|
|
365
|
+
}
|
|
366
|
+
return syncResult;
|
|
347
367
|
case "delete-local":
|
|
348
368
|
if (!file) {
|
|
349
369
|
log(`DELETE LOCAL: ${path} is already absent from storage`, LOG_LEVEL_VERBOSE);
|
|
350
|
-
return
|
|
370
|
+
return FilePairProcessResults.COMPLETED;
|
|
351
371
|
}
|
|
352
372
|
log(`DELETE LOCAL: ${file.path}`, LOG_LEVEL_INFO);
|
|
353
373
|
await host.serviceModules.storageAccess.delete(file.path, true);
|
|
354
374
|
fileMaps.delete(fileMapKey);
|
|
355
375
|
saveFileStatus(host);
|
|
356
|
-
return
|
|
376
|
+
return FilePairProcessResults.COMPLETED;
|
|
357
377
|
case "delete-db": {
|
|
358
378
|
if (!doc) {
|
|
359
379
|
throw new Error(`Missing database entry for ${path}`);
|
|
@@ -363,22 +383,23 @@ async function processFilePair(host, log, pair, options) {
|
|
|
363
383
|
if (dbDeleted) {
|
|
364
384
|
fileMaps.delete(fileMapKey);
|
|
365
385
|
saveFileStatus(host);
|
|
386
|
+
return FilePairProcessResults.COMPLETED;
|
|
366
387
|
} else {
|
|
367
388
|
log(
|
|
368
389
|
`DELETE DATABASE did not delete ${path}; keeping last-seen record to avoid resurrecting the file`,
|
|
369
390
|
LOG_LEVEL_NOTICE
|
|
370
391
|
);
|
|
392
|
+
return FilePairProcessResults.FAILED;
|
|
371
393
|
}
|
|
372
|
-
return true;
|
|
373
394
|
}
|
|
374
395
|
case "skip":
|
|
375
396
|
log(`SKIP ${options.mode}: ${path} (${state})`, LOG_LEVEL_VERBOSE);
|
|
376
|
-
return
|
|
397
|
+
return FilePairProcessResults.SKIPPED;
|
|
377
398
|
}
|
|
378
399
|
} catch (ex) {
|
|
379
400
|
log(`Error processing ${path} with action ${action}`, LOG_LEVEL_NOTICE);
|
|
380
401
|
log(ex, LOG_LEVEL_VERBOSE);
|
|
381
|
-
return
|
|
402
|
+
return FilePairProcessResults.FAILED;
|
|
382
403
|
}
|
|
383
404
|
}
|
|
384
405
|
async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, options) {
|
|
@@ -405,7 +426,9 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
|
|
|
405
426
|
pairs.push(pair);
|
|
406
427
|
}
|
|
407
428
|
log(`Total files to synchronise: ${pairs.length}`, LOG_LEVEL_VERBOSE, "syncAll");
|
|
408
|
-
let
|
|
429
|
+
let completedCount = 0;
|
|
430
|
+
let skippedCount = 0;
|
|
431
|
+
let failedCount = 0;
|
|
409
432
|
let processedCount = 0;
|
|
410
433
|
for await (const result of withConcurrency(
|
|
411
434
|
pairs,
|
|
@@ -415,14 +438,22 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
|
|
|
415
438
|
} catch (ex) {
|
|
416
439
|
log(`Error while synchronising files`, LOG_LEVEL_NOTICE);
|
|
417
440
|
log(ex, LOG_LEVEL_VERBOSE);
|
|
418
|
-
return
|
|
441
|
+
return FilePairProcessResults.FAILED;
|
|
419
442
|
}
|
|
420
443
|
},
|
|
421
444
|
10
|
|
422
445
|
)) {
|
|
423
446
|
processedCount++;
|
|
424
|
-
|
|
425
|
-
|
|
447
|
+
switch (result) {
|
|
448
|
+
case FilePairProcessResults.COMPLETED:
|
|
449
|
+
completedCount++;
|
|
450
|
+
break;
|
|
451
|
+
case FilePairProcessResults.SKIPPED:
|
|
452
|
+
skippedCount++;
|
|
453
|
+
break;
|
|
454
|
+
case FilePairProcessResults.FAILED:
|
|
455
|
+
failedCount++;
|
|
456
|
+
break;
|
|
426
457
|
}
|
|
427
458
|
if (processedCount % 25 === 0) {
|
|
428
459
|
log(
|
|
@@ -433,12 +464,12 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
|
|
|
433
464
|
}
|
|
434
465
|
}
|
|
435
466
|
log(
|
|
436
|
-
`Synchronisation completed: ${
|
|
467
|
+
`Synchronisation completed: ${processedCount} files processed (${completedCount} completed, ${skippedCount} skipped, ${failedCount} failed)`,
|
|
437
468
|
showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO,
|
|
438
469
|
"syncAll"
|
|
439
470
|
);
|
|
440
471
|
saveFileStatus(host, true);
|
|
441
|
-
return
|
|
472
|
+
return failedCount === 0;
|
|
442
473
|
}
|
|
443
474
|
function normaliseFullScanOptions(showingNoticeOrOptions, ignoreSuspending = false) {
|
|
444
475
|
if (typeof showingNoticeOrOptions === "object") {
|
|
@@ -513,7 +544,7 @@ async function performFullScan(host, log, errorManager, showingNoticeOrOptions =
|
|
|
513
544
|
log("Initialize and checking database files");
|
|
514
545
|
log("Checking deleted files");
|
|
515
546
|
await collectDeletedFiles(host, log);
|
|
516
|
-
await synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, options);
|
|
547
|
+
const scanResult = await synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, options);
|
|
517
548
|
log("Initialized, NOW TRACKING!");
|
|
518
549
|
if (!isInitialized) {
|
|
519
550
|
await host.services.keyValueDB.kvDB.set("initialized", true);
|
|
@@ -521,7 +552,7 @@ async function performFullScan(host, log, errorManager, showingNoticeOrOptions =
|
|
|
521
552
|
if (showingNotice) {
|
|
522
553
|
log("Initialize done!", LOG_LEVEL_NOTICE, "syncAll");
|
|
523
554
|
}
|
|
524
|
-
return
|
|
555
|
+
return scanResult;
|
|
525
556
|
}
|
|
526
557
|
function useOfflineScanner(host) {
|
|
527
558
|
const log = createInstanceLogFunction("SF:OfflineScanner", host.services.API);
|
|
@@ -534,6 +565,7 @@ function useOfflineScanner(host) {
|
|
|
534
565
|
export {
|
|
535
566
|
ExtraOnLocal,
|
|
536
567
|
ExtraOnRemote,
|
|
568
|
+
FilePairProcessResults,
|
|
537
569
|
FullScanModes,
|
|
538
570
|
canProceedScan,
|
|
539
571
|
collectDatabaseFiles,
|
|
@@ -262,6 +262,7 @@ Are you sure you wish to proceed?`;
|
|
|
262
262
|
await this.suspendReflectingDatabase(!autoResume);
|
|
263
263
|
await this.control.applySettings();
|
|
264
264
|
await this.resetLocalDatabase();
|
|
265
|
+
this.clearFastFetchCheckpoint();
|
|
265
266
|
await delay(1e3);
|
|
266
267
|
await this.database.openDatabase({
|
|
267
268
|
databaseEvents: this.databaseEvents,
|
|
@@ -300,6 +301,14 @@ Are you sure you wish to proceed?`;
|
|
|
300
301
|
await this.fetchLocal(false, true, autoResume);
|
|
301
302
|
return;
|
|
302
303
|
}
|
|
304
|
+
if (settings.useRequestAPI) {
|
|
305
|
+
this._log(
|
|
306
|
+
"Fast database fetch is unavailable while 'Use Internal API' is enabled. Falling back to Standard Fetch.",
|
|
307
|
+
LOG_LEVEL_NOTICE
|
|
308
|
+
);
|
|
309
|
+
await this.fetchLocal(false, true, autoResume);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
303
312
|
await this.replicator.runBoundedRemoteActivity(() => this.performFetchLocalDBFast(settings, autoResume), {
|
|
304
313
|
label: "fast-fetch"
|
|
305
314
|
});
|
package/docs/development.md
CHANGED
|
@@ -57,7 +57,7 @@ The settings lifecycle has an additional focused unit table in `src/common/model
|
|
|
57
57
|
|
|
58
58
|
The multiple-remote profile contract has focused unit coverage in `src/serviceFeatures/remoteConfig.unit.spec.ts`. Extend it whenever profile identity, generated display names, activation, P2P selection, URI serialisation, or legacy migration changes. New hosts should import the reviewed primitives through `/remote-configurations`; `compat/serviceFeatures/remoteConfig` remains the wider migration surface used by existing clients.
|
|
59
59
|
|
|
60
|
-
The full offline scanner applies both the host's target-file policy and Commonlib's built-in `shouldBeIgnored` policy before constructing storage/database pairs. This keeps legacy LiveSync log files and flag files outside ordinary create, restore, and delete decisions even when a host accepts their file extension. Lower-level write guards remain a defence for direct calls; they are not the normal full-scan filter.
|
|
60
|
+
The full offline scanner applies both the host's target-file policy and Commonlib's built-in `shouldBeIgnored` policy before constructing storage/database pairs. This keeps legacy LiveSync log files and flag files outside ordinary create, restore, and delete decisions even when a host accepts their file extension. Lower-level write guards remain a defence for direct calls; they are not the normal full-scan filter. The complete pair-state decision matrix and database-to-storage reflection result contract are documented in [Full offline scan](offline-scanner.md). Extend that document, the Cartesian action test, and both collection-side tests whenever the policy changes.
|
|
61
61
|
|
|
62
62
|
The revision-tree safety rules are documented in [Conflict resolution and file provenance](conflict-resolution.md). `ConflictManager.unit.spec.ts` owns common-ancestor and conservative merge behaviour, deterministic pair selection for multiple live leaves, and reconstruction from the committed tree between stages. `ServiceDatabaseFileAccessBase.unit.spec.ts` owns all-branch content discovery, short-circuiting history existence checks after an exact match, and exact-revision logical deletion. `ServiceFileHandlerBase.unit.spec.ts` owns the injected provenance policy for edit, deletion, and rename operations, together with user-selected reflection, child creation, strict matching, matching-provenance cleanup after exact branch deletion, and stale-revision refusal. `LiveSyncLocalDB.unit.spec.ts` owns chunk reachability across current winners, shared chunks, live conflicts, available merge ancestry, conflict resolution, and PouchDB replication of collection and recreation. Extend the relevant sides when changing conflict resolution, compaction handling, provenance, chunk collection, or database-to-storage overwrite protection.
|
|
63
63
|
|
package/docs/proven-in-use.md
CHANGED
|
@@ -22,6 +22,8 @@ Commonlib's injected CouchDB tests verify that a mixed remote chunk batch preser
|
|
|
22
22
|
|
|
23
23
|
Commonlib's remote-preference result contract distinguishes available synchronisation settings, a remote which has not stored those settings yet, a failed read, and a remote type which does not support central preferences. Focused CouchDB and Object Storage tests cover absent milestone data separately from transport failures, while Self-hosted LiveSync owns the initialisation choices, cancellation policy, and user-facing explanation.
|
|
24
24
|
|
|
25
|
+
Commonlib's full offline scanner distinguishes completed, deliberately skipped, and failed storage/database pairs. It continues processing after individual failures, returns a failed aggregate only for actual failures, and records a database mtime as local provenance only after database content has been reflected successfully. Self-hosted LiveSync consumes the aggregate directly during Fast Setup, where an actual reflection failure reaches the host-owned recovery choice without treating conflict or size-policy skips as failures.
|
|
26
|
+
|
|
25
27
|
Most replication and storage services used by the plug-in still enter through explicit `compat/*` paths. Those imports prove that the published compatibility boundary supports the current migration; they are not examples of a finished high-level client API.
|
|
26
28
|
|
|
27
29
|
The plug-in's manual onboarding and Remote Databases pane use the focused `/remote-configurations` entry to create opaque profile IDs, retain user-visible names, and select main and P2P remotes. Commonlib owns the in-memory profile result contract; Self-hosted LiveSync adds dialogue, Setup URI classification, Fetch or Rebuild scheduling, persistence, restart ordering, and real-Obsidian presentation checks.
|
|
@@ -38,6 +40,8 @@ The CLI filesystem composition receives a configured base directory and construc
|
|
|
38
40
|
|
|
39
41
|
The maintained CLI checks include unit tests, Deno-driven CLI E2E, Compose-based compatibility runs, and the CLI-to-real-Obsidian synchronisation scenario in `test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts`. The latter is the strongest current evidence that the CLI and plug-in compose compatible Commonlib behaviour across two hosts.
|
|
40
42
|
|
|
43
|
+
The CLI mirror command returns Commonlib's full-scan aggregate, and daemon start-up stops when that aggregate reports an actual pair failure. Deliberately skipped pairs remain a successful scan result. Commonlib owns the pair classification and aggregate; the CLI owns its command and process policy.
|
|
44
|
+
|
|
41
45
|
## WebApp
|
|
42
46
|
|
|
43
47
|
WebApp receives an already authorised `FileSystemDirectoryHandle` from its host and constructs `FileSystemAccessStorageAdapter` inside `src/apps/webapp/adapters/FSAPIFileSystemAdapter.ts`. The adapter does not call `showDirectoryPicker()`, choose a directory, or persist the handle. This is the maintained application example for the browser entry's root-ownership rule.
|