@vrtmrz/livesync-commonlib 0.1.44 → 0.2.1
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/README.md +80 -4
- package/dist/API/DirectFileManipulatorV2.d.ts +2 -0
- package/dist/API/DirectFileManipulatorV2.js +3 -0
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/common/configForDoc.d.ts +1 -0
- package/dist/common/configForDoc.js +11 -1
- package/dist/common/models/db.definition.d.ts +1 -0
- package/dist/common/models/setting.const.defaults.js +1 -0
- package/dist/common/models/setting.const.preferred.js +1 -3
- package/dist/common/models/setting.const.qr.js +1 -0
- package/dist/common/models/setting.policy.d.ts +4 -0
- package/dist/common/models/setting.policy.js +1 -0
- package/dist/common/models/setting.type.d.ts +2 -0
- package/dist/common/models/shared.definition.d.ts +20 -0
- package/dist/common/models/tweak.definition.d.ts +1 -0
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/settingConstants.js +6 -2
- package/dist/interfaces/FileHandler.d.ts +5 -1
- package/dist/interfaces/StorageEventManager.d.ts +5 -0
- package/dist/interfaces/StorageEventManager.js +7 -0
- package/dist/managers/EntryManager/EntryManagerImpls.js +3 -2
- package/dist/managers/StorageEventManager.d.ts +19 -10
- package/dist/managers/StorageEventManager.js +259 -189
- package/dist/pouchdb/LiveSyncDBFunctions.js +4 -0
- package/dist/pouchdb/StreamingFetch.d.ts +5 -1
- package/dist/pouchdb/StreamingFetch.js +529 -65
- package/dist/replication/LiveSyncAbstractReplicator.d.ts +2 -0
- package/dist/replication/LiveSyncAbstractReplicator.js +5 -2
- package/dist/replication/RemoteResource.d.ts +23 -2
- package/dist/replication/RemoteResource.js +6 -2
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +14 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +171 -3
- package/dist/replication/couchdb/fastForward.d.ts +19 -0
- package/dist/replication/couchdb/fastForward.js +119 -0
- package/dist/replication/couchdb/replicationProgress.d.ts +6 -0
- package/dist/replication/couchdb/replicationProgress.js +105 -0
- package/dist/replication/index.d.ts +4 -1
- package/dist/replication/joinedDevices.d.ts +25 -0
- package/dist/replication/joinedDevices.js +123 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +2 -1
- package/dist/replication/journal/JournalSyncCore.js +11 -1
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +4 -0
- package/dist/replication/journal/LiveSyncJournalReplicator.js +56 -1
- package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +3 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -1
- package/dist/replication/journal/objectstore/MinioStorageAdapter.js +27 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +1 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.js +27 -14
- package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
- package/dist/serviceFeatures/offlineScanner.js +19 -7
- package/dist/serviceModules/Rebuilder.js +41 -15
- package/dist/serviceModules/ServiceFileAccessBase.js +5 -0
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +8 -5
- package/dist/serviceModules/ServiceFileHandlerBase.js +75 -38
- package/dist/services/BrowserServices.d.ts +4 -2
- package/dist/services/BrowserServices.js +2 -5
- package/dist/services/HeadlessServices.d.ts +3 -0
- package/dist/services/HeadlessServices.js +2 -5
- package/dist/services/ServiceHub.d.ts +4 -4
- package/dist/services/base/AppLifecycleService.js +1 -1
- package/dist/services/base/ControlService.js +6 -2
- package/dist/services/base/FileProcessingService.d.ts +7 -1
- package/dist/services/base/FileProcessingService.js +9 -1
- package/dist/services/base/IService.d.ts +19 -19
- package/dist/services/base/ReplicatorService.d.ts +1 -0
- package/dist/services/base/ReplicatorService.remoteResourceResolver.js +1 -1
- package/dist/services/base/SettingService.d.ts +8 -18
- package/dist/services/base/SettingService.js +28 -46
- package/dist/services/implements/base/SvelteDialog.d.ts +2 -2
- package/dist/services/implements/base/UIService.d.ts +2 -0
- package/dist/services/implements/base/UIService.js +5 -0
- package/dist/services/implements/injectable/InjectableServiceHub.d.ts +4 -4
- package/dist/services/implements/injectable/InjectableServices.d.ts +2 -2
- package/dist/services/implements/injectable/InjectableSettingService.d.ts +0 -3
- package/dist/services/implements/injectable/InjectableSettingService.js +0 -11
- package/dist/services/lib/HandlerUtils.d.ts +60 -0
- package/dist/services/lib/HandlerUtils.js +46 -0
- package/dist/string_and_binary/chunks.js +7 -5
- package/dist/uiCommands.d.ts +26 -0
- package/dist/uiCommands.js +57 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/development.md +15 -3
- package/docs/offline-scanner.md +15 -13
- package/docs/proven-in-use.md +38 -4
- package/docs/releasing.md +20 -13
- package/docs/settings-lifecycle.md +39 -5
- package/docs/storage-events-and-reflection.md +109 -10
- package/package.json +13 -18
- package/dist/services/base/ConfigService.d.ts +0 -7
- package/dist/services/base/ConfigService.js +0 -6
- package/dist/services/implements/browser/ConfigServiceBrowserCompat.d.ts +0 -16
- package/dist/services/implements/browser/ConfigServiceBrowserCompat.js +0 -23
|
@@ -3,10 +3,13 @@ export type { ReplicatorInstance } from "./ReplicatorInstance.js";
|
|
|
3
3
|
export type { ActiveReplicatorContext, CapabilitySupport, ContinuousReplicationRequest, ContinuousReplicationTrigger, ContinuousRunner, InteractionAuthority, InteractionPermissions, NotApplicableCapability, NotImplementedCapability, NoInteraction, PermittedInteractionPermissions, ReplicationBlockReason, ReplicationAttemptFailure, ReplicationCancelled, ReplicationCompleted, ReplicationFailed, ReplicationInteraction, ReplicationOutcome, ReplicationProgressPresentation, ReplicationReadinessRequirements, ReplicationPartial, ReplicationFailureRequest, ReplicatorConfigurationIdentity, ReplicatorProviderDefinition, ReplicatorProviderDefinitionMap, StopActiveTransferRunner, SupportedCapability, UnattendedOneShotRequest, UnattendedReplicationTrigger, UnattendedOneShotRunner, UserInitiatedOneShotRequest, UserInitiatedReplicationAuthority, UserInitiatedOneShotRunner, } from "./ReplicatorProvider.js";
|
|
4
4
|
export { CAPABILITY_SUPPORT_KINDS, CAPABILITY_UNAVAILABLE_REASONS } from "./ProviderCapability.js";
|
|
5
5
|
export { NO_REMOTE_RESOURCE_CAPABILITIES, REMOTE_RESOURCE_KINDS } from "./RemoteResource.js";
|
|
6
|
-
export type { ConnectionProbeFactory, PreferredTweakProbe, PreferredTweakProbeFactory, RemoteConnectionProbe, RemoteConnectionProbeOptions, RemoteConnectionProbeResult, RemoteResourceCapabilities, RemoteResourceFactoryMap, RemoteResourceKind, RemoteResourceMap, SecuritySeedResource, SecuritySeedResourceFactory, SynchronisationInformationResource, SynchronisationInformationResourceFactory, } from "./RemoteResource.js";
|
|
6
|
+
export type { InitialFetchFastForwardResource, InitialFetchFastForwardResourceFactory, ConnectionProbeFactory, JoinedDevicesResource, JoinedDevicesResourceFactory, PreferredTweakProbe, PreferredTweakProbeFactory, RemoteConnectionProbe, RemoteConnectionProbeOptions, RemoteConnectionProbeResult, RemoteResourceCapabilities, RemoteResourceFactoryMap, RemoteResourceKind, RemoteResourceMap, SecuritySeedResource, SecuritySeedResourceFactory, SynchronisationInformationResource, SynchronisationInformationResourceFactory, } from "./RemoteResource.js";
|
|
7
|
+
export type { InitialFetchFastForward } from "./couchdb/fastForward.js";
|
|
7
8
|
export { CENTRAL_REMOTE_ADMINISTRATION_ACTIONS, CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS, CENTRAL_REMOTE_ADMINISTRATION_OBSERVATION_KINDS, CENTRAL_REMOTE_ADMINISTRATION_RESULT_STATUSES, applyCentralRemoteAdministrationMutation, isCentralRemoteAdministrationVerified, milestoneSatisfiesCentralRemoteAdministration, centralRemoteAdministrationVerificationFailed, centralRemoteAdministrationVerified, } from "./CentralRemoteAdministration.js";
|
|
8
9
|
export type { MilestoneCentralRemoteAdministrationObservation, CentralRemoteAdministrationAction, CentralRemoteAdministrationFailureReason, CentralRemoteAdministrationObservation, CentralRemoteAdministrationRequest, CentralRemoteAdministrationReplicator, CentralRemoteAdministrationResult, CentralRemoteAdministrationRunner, CentralRemoteAdministrationVerificationFailed, CentralRemoteAdministrationVerified, } from "./CentralRemoteAdministration.js";
|
|
9
10
|
export { CENTRAL_COMPATIBILITY_REJECTION_REASONS } from "./CentralCompatibility.js";
|
|
10
11
|
export type { CentralCompatibilityRecoveryHint, CentralCompatibilityRejectionReason } from "./CentralCompatibility.js";
|
|
11
12
|
export { assessRemoteFeatureDocument, describeRemoteFeatureRejection, ENCRYPTED_INTERNAL_METADATA_FEATURE, NFC_PATH_IDENTITY_FEATURE, REMOTE_FEATURE_GENERATION, usesEncryptedInternalMetadata, } from "../pouchdb/remoteFeatureCompatibility.js";
|
|
12
13
|
export type { RemoteFeatureAssessment } from "../pouchdb/remoteFeatureCompatibility.js";
|
|
14
|
+
export type { JoinedDevice, JoinedDevicesSnapshot, ForgetJoinedDeviceResult } from "./joinedDevices.js";
|
|
15
|
+
export type { ReplicationStatics, CouchDBReplicationPhase, CouchDBReplicationProgress, ReplicationCheckpointProgress, } from "../common/models/shared.definition.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type EntryMilestoneInfo } from "../common/types.js";
|
|
2
|
+
export interface JoinedDevice {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly accepted: boolean;
|
|
5
|
+
readonly platform?: string;
|
|
6
|
+
readonly pluginVersion?: string;
|
|
7
|
+
readonly appVersion?: string;
|
|
8
|
+
readonly lastConnected?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface JoinedDevicesSnapshot {
|
|
11
|
+
readonly milestonePresent: boolean;
|
|
12
|
+
readonly locked: boolean;
|
|
13
|
+
readonly cleaned: boolean;
|
|
14
|
+
readonly devices: readonly JoinedDevice[];
|
|
15
|
+
}
|
|
16
|
+
export declare function projectJoinedDevices(milestone: EntryMilestoneInfo | undefined): JoinedDevicesSnapshot;
|
|
17
|
+
export type ForgetJoinedDeviceResult = "forgotten" | "not-found" | "conflict" | "unsupported";
|
|
18
|
+
export interface JoinedDevicesStore {
|
|
19
|
+
read(): Promise<{
|
|
20
|
+
milestone: EntryMilestoneInfo;
|
|
21
|
+
revision?: string;
|
|
22
|
+
} | undefined>;
|
|
23
|
+
write(milestone: EntryMilestoneInfo, revision: string): Promise<"updated" | "conflict" | "unsupported">;
|
|
24
|
+
}
|
|
25
|
+
export declare function forgetJoinedDevice(store: JoinedDevicesStore, deviceId: string, ownDeviceId: string): Promise<ForgetJoinedDeviceResult>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { DEVICE_ID_PREFERRED } from "../common/types.js";
|
|
2
|
+
function projectJoinedDevices(milestone) {
|
|
3
|
+
if (milestone === void 0) {
|
|
4
|
+
return { milestonePresent: false, locked: false, cleaned: false, devices: [] };
|
|
5
|
+
}
|
|
6
|
+
const source = milestone;
|
|
7
|
+
const nodeInfo = asRecord(source.node_info);
|
|
8
|
+
const acceptedNodes = Array.isArray(source.accepted_nodes) ? source.accepted_nodes.filter((id) => typeof id === "string") : [];
|
|
9
|
+
const accepted = new Set(acceptedNodes);
|
|
10
|
+
const ids = /* @__PURE__ */ new Set();
|
|
11
|
+
for (const id of acceptedNodes) {
|
|
12
|
+
if (id !== DEVICE_ID_PREFERRED) ids.add(id);
|
|
13
|
+
}
|
|
14
|
+
addRecordKeys(ids, source.node_info);
|
|
15
|
+
addRecordKeys(ids, source.node_chunk_info);
|
|
16
|
+
addRecordKeys(ids, source.tweak_values);
|
|
17
|
+
const devices = [...ids].filter((id) => id !== DEVICE_ID_PREFERRED).sort().map((id) => {
|
|
18
|
+
const nodeData = nodeInfo && hasOwn(nodeInfo, id) ? asRecord(nodeInfo[id]) : void 0;
|
|
19
|
+
const platform = readString(nodeData, "platform");
|
|
20
|
+
const pluginVersion = readString(nodeData, "plugin_version");
|
|
21
|
+
const appVersion = readString(nodeData, "app_version");
|
|
22
|
+
const lastConnected = readTimestamp(nodeData?.last_connected);
|
|
23
|
+
return {
|
|
24
|
+
id,
|
|
25
|
+
accepted: accepted.has(id),
|
|
26
|
+
...platform === void 0 ? {} : { platform },
|
|
27
|
+
...pluginVersion === void 0 ? {} : { pluginVersion },
|
|
28
|
+
...appVersion === void 0 ? {} : { appVersion },
|
|
29
|
+
...lastConnected === void 0 ? {} : { lastConnected }
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
milestonePresent: true,
|
|
34
|
+
locked: source.locked === true,
|
|
35
|
+
cleaned: source.cleaned === true,
|
|
36
|
+
devices
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const MAX_CONFLICT_RETRIES = 3;
|
|
40
|
+
async function forgetJoinedDevice(store, deviceId, ownDeviceId) {
|
|
41
|
+
if (ownDeviceId.length === 0) {
|
|
42
|
+
throw new Error("The current device ID must not be empty");
|
|
43
|
+
}
|
|
44
|
+
if (deviceId.length === 0) {
|
|
45
|
+
throw new Error("The device ID must not be empty");
|
|
46
|
+
}
|
|
47
|
+
if (deviceId === DEVICE_ID_PREFERRED) {
|
|
48
|
+
throw new Error("The preferred-device marker cannot be forgotten");
|
|
49
|
+
}
|
|
50
|
+
if (deviceId === ownDeviceId) {
|
|
51
|
+
throw new Error("The current device cannot be forgotten");
|
|
52
|
+
}
|
|
53
|
+
let conflictRetries = 0;
|
|
54
|
+
while (true) {
|
|
55
|
+
const current = await store.read();
|
|
56
|
+
if (current === void 0 || !hasDevice(current.milestone, deviceId)) {
|
|
57
|
+
return "not-found";
|
|
58
|
+
}
|
|
59
|
+
if (typeof current.revision !== "string") {
|
|
60
|
+
return "unsupported";
|
|
61
|
+
}
|
|
62
|
+
const updatedMilestone = removeDevice(current.milestone, deviceId);
|
|
63
|
+
const result = await store.write(updatedMilestone, current.revision);
|
|
64
|
+
if (result === "updated") return "forgotten";
|
|
65
|
+
if (result === "unsupported") return "unsupported";
|
|
66
|
+
if (conflictRetries >= MAX_CONFLICT_RETRIES) return "conflict";
|
|
67
|
+
conflictRetries++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function asRecord(value) {
|
|
71
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function hasOwn(record, key) {
|
|
75
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
76
|
+
}
|
|
77
|
+
function addRecordKeys(ids, value) {
|
|
78
|
+
const record = asRecord(value);
|
|
79
|
+
if (!record) return;
|
|
80
|
+
for (const id of Object.keys(record)) {
|
|
81
|
+
if (id !== DEVICE_ID_PREFERRED) ids.add(id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function readString(record, key) {
|
|
85
|
+
const value = record?.[key];
|
|
86
|
+
return typeof value === "string" ? value : void 0;
|
|
87
|
+
}
|
|
88
|
+
function readTimestamp(value) {
|
|
89
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
90
|
+
}
|
|
91
|
+
function hasDevice(milestone, deviceId) {
|
|
92
|
+
const source = milestone;
|
|
93
|
+
const accepted = Array.isArray(source.accepted_nodes) && source.accepted_nodes.includes(deviceId);
|
|
94
|
+
return accepted || hasMapKey(source.node_info, deviceId) || hasMapKey(source.node_chunk_info, deviceId) || hasMapKey(source.tweak_values, deviceId);
|
|
95
|
+
}
|
|
96
|
+
function hasMapKey(value, deviceId) {
|
|
97
|
+
const record = asRecord(value);
|
|
98
|
+
return record !== void 0 && hasOwn(record, deviceId);
|
|
99
|
+
}
|
|
100
|
+
function removeDevice(milestone, deviceId) {
|
|
101
|
+
const source = milestone;
|
|
102
|
+
const updated = { ...source };
|
|
103
|
+
for (const key of ["node_info", "node_chunk_info", "tweak_values"]) {
|
|
104
|
+
if (hasOwn(source, key)) {
|
|
105
|
+
updated[key] = removeMapKey(source[key], deviceId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (hasOwn(source, "accepted_nodes") && Array.isArray(source.accepted_nodes)) {
|
|
109
|
+
updated.accepted_nodes = source.accepted_nodes.filter((id) => id !== deviceId);
|
|
110
|
+
}
|
|
111
|
+
return updated;
|
|
112
|
+
}
|
|
113
|
+
function removeMapKey(value, deviceId) {
|
|
114
|
+
const record = asRecord(value);
|
|
115
|
+
if (!record) return value;
|
|
116
|
+
const updated = { ...record };
|
|
117
|
+
delete updated[deviceId];
|
|
118
|
+
return updated;
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
forgetJoinedDevice,
|
|
122
|
+
projectJoinedDevices
|
|
123
|
+
};
|
|
@@ -3,7 +3,7 @@ import type { ReplicationCallback, ReplicationStat } from "../LiveSyncAbstractRe
|
|
|
3
3
|
import { type SimpleStore } from "../../common/utils.js";
|
|
4
4
|
import { type CheckPointInfo } from "./JournalSyncTypes.js";
|
|
5
5
|
import type { LiveSyncJournalReplicatorEnv } from "./LiveSyncJournalReplicatorEnv.js";
|
|
6
|
-
import { type IJournalStorage, type JournalStorageReadResult } from "./objectstore/JournalStorageAdapter.js";
|
|
6
|
+
import { type IJournalStorage, type JournalStorageConditionalWriteResult, type JournalStorageReadResult } from "./objectstore/JournalStorageAdapter.js";
|
|
7
7
|
type ProcessingEntry = PouchDB.Core.PutDocument<EntryDoc> & PouchDB.Core.GetMeta;
|
|
8
8
|
export declare class JournalSyncCore {
|
|
9
9
|
_settings: BucketSyncSetting;
|
|
@@ -24,6 +24,7 @@ export declare class JournalSyncCore {
|
|
|
24
24
|
constructor(settings: BucketSyncSetting, store: SimpleStore<CheckPointInfo>, env: LiveSyncJournalReplicatorEnv, storage: IJournalStorage);
|
|
25
25
|
downloadJson<T>(key: string): Promise<T | false>;
|
|
26
26
|
downloadJsonWithResult<T>(key: string): Promise<JournalStorageReadResult<T>>;
|
|
27
|
+
uploadJsonIfUnchanged<T>(key: string, body: T, revision: string): Promise<JournalStorageConditionalWriteResult>;
|
|
27
28
|
uploadJson<T>(key: string, body: T): Promise<boolean>;
|
|
28
29
|
applyNewConfig(settings: BucketSyncSetting, store: SimpleStore<CheckPointInfo>, env: LiveSyncJournalReplicatorEnv): void;
|
|
29
30
|
updateInfo(info: Partial<ReplicationStat>): void;
|
|
@@ -127,7 +127,8 @@ class JournalSyncCore {
|
|
|
127
127
|
try {
|
|
128
128
|
return {
|
|
129
129
|
status: JournalStorageReadStatuses.AVAILABLE,
|
|
130
|
-
value: JSON.parse(new TextDecoder().decode(result.value))
|
|
130
|
+
value: JSON.parse(new TextDecoder().decode(result.value)),
|
|
131
|
+
...result.revision === void 0 ? {} : { revision: result.revision }
|
|
131
132
|
};
|
|
132
133
|
} catch (ex) {
|
|
133
134
|
Logger(`Could not parse downloaded json ${key}`);
|
|
@@ -138,6 +139,15 @@ class JournalSyncCore {
|
|
|
138
139
|
};
|
|
139
140
|
}
|
|
140
141
|
}
|
|
142
|
+
async uploadJsonIfUnchanged(key, body, revision) {
|
|
143
|
+
if (revision.trim().length === 0) {
|
|
144
|
+
throw new TypeError("A non-empty revision is required for a conditional upload");
|
|
145
|
+
}
|
|
146
|
+
const uploadIfUnchanged = this.storage.uploadIfUnchanged;
|
|
147
|
+
if (!uploadIfUnchanged) return "unsupported";
|
|
148
|
+
const data = new TextEncoder().encode(JSON.stringify(body));
|
|
149
|
+
return await uploadIfUnchanged.call(this.storage, key, data, "application/json", revision);
|
|
150
|
+
}
|
|
141
151
|
async uploadJson(key, body) {
|
|
142
152
|
try {
|
|
143
153
|
const data = new TextEncoder().encode(JSON.stringify(body));
|
|
@@ -7,6 +7,7 @@ import type { SimpleStore } from "../../common/utils.js";
|
|
|
7
7
|
import type { LiveSyncJournalReplicatorEnv } from "./LiveSyncJournalReplicatorEnv.js";
|
|
8
8
|
import { type CentralCompatibilityDecisionRecorder } from "../CentralCompatibility.js";
|
|
9
9
|
import { type ReplicationOutcome } from "../ReplicatorProvider.js";
|
|
10
|
+
import { type ForgetJoinedDeviceResult, type JoinedDevicesSnapshot } from "../joinedDevices.js";
|
|
10
11
|
export declare class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
|
|
11
12
|
env: LiveSyncJournalReplicatorEnv;
|
|
12
13
|
get client(): JournalSyncCore;
|
|
@@ -63,6 +64,9 @@ export declare class LiveSyncJournalReplicator extends LiveSyncAbstractReplicato
|
|
|
63
64
|
getRemoteStatus(setting: RemoteDBSettings): Promise<false | RemoteDBStatus>;
|
|
64
65
|
private supportsLegacyPathIdentity;
|
|
65
66
|
private assertLegacyPathIdentity;
|
|
67
|
+
private getOwnJoinedDeviceId;
|
|
68
|
+
getJoinedDevices(setting: RemoteDBSettings): Promise<JoinedDevicesSnapshot>;
|
|
69
|
+
forgetJoinedDevice(setting: RemoteDBSettings, deviceId: string): Promise<ForgetJoinedDeviceResult>;
|
|
66
70
|
getConnectedDeviceList(setting?: RemoteDBSettings): Promise<false | {
|
|
67
71
|
node_info: Record<string, NodeData>;
|
|
68
72
|
accepted_nodes: string[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
NODEINFO_DOCID,
|
|
2
3
|
LOG_LEVEL_NOTICE,
|
|
3
4
|
LOG_LEVEL_VERBOSE,
|
|
4
5
|
DEVICE_ID_PREFERRED,
|
|
@@ -26,6 +27,10 @@ import {
|
|
|
26
27
|
outcomeFromFiniteOpenReplication,
|
|
27
28
|
replicationFailed
|
|
28
29
|
} from "../ReplicatorProvider.js";
|
|
30
|
+
import {
|
|
31
|
+
forgetJoinedDevice as forgetJoinedDeviceFromStore,
|
|
32
|
+
projectJoinedDevices
|
|
33
|
+
} from "../joinedDevices.js";
|
|
29
34
|
const MILSTONE_DOCID = "_00000000-milestone.json";
|
|
30
35
|
async function readRemoteMilestone(client) {
|
|
31
36
|
const result = await client.downloadJsonWithResult(MILSTONE_DOCID);
|
|
@@ -113,7 +118,8 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
|
|
|
113
118
|
plugin_version: this.env.services.API.getPluginVersion(),
|
|
114
119
|
vault_name: this.env.services.vault.vaultName(),
|
|
115
120
|
device_name: this.env.services.vault.getVaultName(),
|
|
116
|
-
progress
|
|
121
|
+
progress,
|
|
122
|
+
platform: this.env.services.API.getPlatform()
|
|
117
123
|
},
|
|
118
124
|
async (info) => {
|
|
119
125
|
if (encryptedProof) info.encrypted_id_derivation_proof = encryptedProof;
|
|
@@ -584,6 +590,55 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
|
|
|
584
590
|
throw new Error("Object Storage does not support NFC path identity.");
|
|
585
591
|
}
|
|
586
592
|
}
|
|
593
|
+
async getOwnJoinedDeviceId() {
|
|
594
|
+
const nodeInfo = await this.rawDatabase.get(NODEINFO_DOCID);
|
|
595
|
+
const nodeId = nodeInfo?.nodeid;
|
|
596
|
+
if (typeof nodeId !== "string" || nodeId.trim().length === 0) {
|
|
597
|
+
throw new Error("The local node identity is unavailable.");
|
|
598
|
+
}
|
|
599
|
+
return nodeId;
|
|
600
|
+
}
|
|
601
|
+
async getJoinedDevices(setting) {
|
|
602
|
+
this.assertLegacyPathIdentity(setting);
|
|
603
|
+
const trialClient = new JournalSyncCore(
|
|
604
|
+
setting,
|
|
605
|
+
this.simpleStore,
|
|
606
|
+
this.env,
|
|
607
|
+
new MinioStorageAdapter(setting, this.env)
|
|
608
|
+
);
|
|
609
|
+
try {
|
|
610
|
+
const result = await trialClient.downloadJsonWithResult(MILSTONE_DOCID);
|
|
611
|
+
if (result.status === JournalStorageReadStatuses.NOT_FOUND) return projectJoinedDevices(void 0);
|
|
612
|
+
if (result.status === JournalStorageReadStatuses.UNAVAILABLE) throw result.error;
|
|
613
|
+
return projectJoinedDevices(result.value);
|
|
614
|
+
} finally {
|
|
615
|
+
trialClient.dispose();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async forgetJoinedDevice(setting, deviceId) {
|
|
619
|
+
this.assertLegacyPathIdentity(setting);
|
|
620
|
+
const ownDeviceId = await this.getOwnJoinedDeviceId();
|
|
621
|
+
const trialClient = new JournalSyncCore(
|
|
622
|
+
setting,
|
|
623
|
+
this.simpleStore,
|
|
624
|
+
this.env,
|
|
625
|
+
new MinioStorageAdapter(setting, this.env)
|
|
626
|
+
);
|
|
627
|
+
try {
|
|
628
|
+
const store = {
|
|
629
|
+
read: async () => {
|
|
630
|
+
const result = await trialClient.downloadJsonWithResult(MILSTONE_DOCID);
|
|
631
|
+
if (result.status === JournalStorageReadStatuses.NOT_FOUND) return void 0;
|
|
632
|
+
if (result.status === JournalStorageReadStatuses.UNAVAILABLE) throw result.error;
|
|
633
|
+
return { milestone: result.value, revision: result.revision };
|
|
634
|
+
},
|
|
635
|
+
write: async (milestone, revision) => await trialClient.uploadJsonIfUnchanged(MILSTONE_DOCID, milestone, revision)
|
|
636
|
+
};
|
|
637
|
+
return await forgetJoinedDeviceFromStore(store, deviceId, ownDeviceId);
|
|
638
|
+
} finally {
|
|
639
|
+
trialClient.dispose();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
587
642
|
getConnectedDeviceList(setting) {
|
|
588
643
|
return Promise.resolve(false);
|
|
589
644
|
}
|
|
@@ -8,16 +8,19 @@ export declare const JournalStorageReadStatuses: {
|
|
|
8
8
|
export type JournalStorageReadResult<T> = {
|
|
9
9
|
status: typeof JournalStorageReadStatuses.AVAILABLE;
|
|
10
10
|
value: T;
|
|
11
|
+
readonly revision?: string;
|
|
11
12
|
} | {
|
|
12
13
|
status: typeof JournalStorageReadStatuses.NOT_FOUND;
|
|
13
14
|
} | {
|
|
14
15
|
status: typeof JournalStorageReadStatuses.UNAVAILABLE;
|
|
15
16
|
error: unknown;
|
|
16
17
|
};
|
|
18
|
+
export type JournalStorageConditionalWriteResult = "updated" | "conflict" | "unsupported";
|
|
17
19
|
export interface IJournalStorage {
|
|
18
20
|
/** Release transport resources retained by this adapter, when any. */
|
|
19
21
|
dispose?(): void;
|
|
20
22
|
upload(key: string, data: Uint8Array, mime: string): Promise<boolean>;
|
|
23
|
+
uploadIfUnchanged?(key: string, data: Uint8Array, mime: string, revision: string): Promise<JournalStorageConditionalWriteResult>;
|
|
21
24
|
download(key: string, ignoreCache?: boolean): Promise<Uint8Array | false>;
|
|
22
25
|
downloadWithResult(key: string, ignoreCache?: boolean): Promise<JournalStorageReadResult<Uint8Array>>;
|
|
23
26
|
listFiles(from: string, limit?: number): Promise<string[]>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { S3 } from "@aws-sdk/client-s3";
|
|
2
2
|
import { type BucketSyncSetting } from "../../../common/types.js";
|
|
3
3
|
import type { RemoteDBStatus } from "../../LiveSyncAbstractReplicator.js";
|
|
4
|
-
import { type IJournalStorage, type JournalStorageReadResult } from "./JournalStorageAdapter.js";
|
|
4
|
+
import { type IJournalStorage, type JournalStorageConditionalWriteResult, type JournalStorageReadResult } from "./JournalStorageAdapter.js";
|
|
5
5
|
import type { LiveSyncJournalReplicatorEnv } from "../LiveSyncJournalReplicatorEnv.js";
|
|
6
6
|
export declare class MinioStorageAdapter implements IJournalStorage {
|
|
7
7
|
_instance?: S3;
|
|
@@ -15,6 +15,7 @@ export declare class MinioStorageAdapter implements IJournalStorage {
|
|
|
15
15
|
get customHeaders(): [string, string][];
|
|
16
16
|
_getClient(): S3;
|
|
17
17
|
upload(key: string, data: Uint8Array, mime: string): Promise<boolean>;
|
|
18
|
+
uploadIfUnchanged(key: string, data: Uint8Array, mime: string, revision: string): Promise<JournalStorageConditionalWriteResult>;
|
|
18
19
|
downloadWithResult(key: string, ignoreCache?: boolean): Promise<JournalStorageReadResult<Uint8Array>>;
|
|
19
20
|
download(key: string, ignoreCache?: boolean): Promise<Uint8Array | false>;
|
|
20
21
|
listFiles(from: string, limit?: number): Promise<string[]>;
|
|
@@ -119,6 +119,26 @@ class MinioStorageAdapter {
|
|
|
119
119
|
}
|
|
120
120
|
return false;
|
|
121
121
|
}
|
|
122
|
+
async uploadIfUnchanged(key, data, mime, revision) {
|
|
123
|
+
if (revision.trim().length === 0) {
|
|
124
|
+
throw new TypeError("A non-empty revision is required for a conditional upload");
|
|
125
|
+
}
|
|
126
|
+
const client = this._getClient();
|
|
127
|
+
const cmd = new PutObjectCommand({
|
|
128
|
+
Bucket: this._settings.bucket,
|
|
129
|
+
Key: `${this._settings.bucketPrefix}${key}`,
|
|
130
|
+
Body: data,
|
|
131
|
+
ContentType: mime,
|
|
132
|
+
IfMatch: revision
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
await this.runTrackedRequest(() => client.send(cmd));
|
|
136
|
+
return "updated";
|
|
137
|
+
} catch (ex) {
|
|
138
|
+
if (isConditionalWriteConflict(ex)) return "conflict";
|
|
139
|
+
throw ex;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
122
142
|
async downloadWithResult(key, ignoreCache = false) {
|
|
123
143
|
const client = this._getClient();
|
|
124
144
|
const cmd = new GetObjectCommand({
|
|
@@ -135,7 +155,8 @@ class MinioStorageAdapter {
|
|
|
135
155
|
if (r.Body) {
|
|
136
156
|
return {
|
|
137
157
|
status: JournalStorageReadStatuses.AVAILABLE,
|
|
138
|
-
value: new Uint8Array(await r.Body.transformToByteArray())
|
|
158
|
+
value: new Uint8Array(await r.Body.transformToByteArray()),
|
|
159
|
+
...r.ETag?.trim() ? { revision: r.ETag } : {}
|
|
139
160
|
};
|
|
140
161
|
}
|
|
141
162
|
return {
|
|
@@ -227,6 +248,11 @@ class MinioStorageAdapter {
|
|
|
227
248
|
}
|
|
228
249
|
}
|
|
229
250
|
}
|
|
251
|
+
function isConditionalWriteConflict(error) {
|
|
252
|
+
if (!error || typeof error !== "object") return false;
|
|
253
|
+
const { httpStatusCode } = error.$metadata ?? {};
|
|
254
|
+
return httpStatusCode === 409 || httpStatusCode === 412;
|
|
255
|
+
}
|
|
230
256
|
function isMissingObjectError(error) {
|
|
231
257
|
if (!error || typeof error !== "object") return false;
|
|
232
258
|
const { name, Code, code } = error;
|
|
@@ -73,7 +73,7 @@ export declare class P2PRoomSessionOwner implements P2PRoomSessionAccess {
|
|
|
73
73
|
private hasUsableTurnServer;
|
|
74
74
|
private withRoomOpenDeadline;
|
|
75
75
|
private assertReconciliationCurrent;
|
|
76
|
-
/**
|
|
76
|
+
/** Read the device identity before capturing the current synchronous session inputs. */
|
|
77
77
|
private getEffectiveBinding;
|
|
78
78
|
/** Apply policy which can change without retiring the room session. */
|
|
79
79
|
private reconcileCurrentSessionPolicy;
|
|
@@ -70,7 +70,7 @@ class P2PRoomSessionOwner {
|
|
|
70
70
|
} else if (active && !supportsPathIdentity) {
|
|
71
71
|
Logger("P2P does not support NFC path identity.", LOG_LEVEL_NOTICE);
|
|
72
72
|
}
|
|
73
|
-
this.fencePreparationForCurrentSettings(enabled);
|
|
73
|
+
await this.fencePreparationForCurrentSettings(enabled);
|
|
74
74
|
await this.reconcileTransport();
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
@@ -89,8 +89,8 @@ class P2PRoomSessionOwner {
|
|
|
89
89
|
}
|
|
90
90
|
const demand = /* @__PURE__ */ Symbol("p2p-finite-room-demand");
|
|
91
91
|
this.finiteDemands.add(demand);
|
|
92
|
-
this.fencePreparationForCurrentSettings(true);
|
|
93
92
|
try {
|
|
93
|
+
await this.fencePreparationForCurrentSettings(true);
|
|
94
94
|
await this.reconcileTransport();
|
|
95
95
|
if (!this.finiteDemands.has(demand)) {
|
|
96
96
|
throw new Error("The P2P room demand was retired before the operation started.");
|
|
@@ -143,7 +143,8 @@ class P2PRoomSessionOwner {
|
|
|
143
143
|
await this.closeTransport();
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
|
-
const binding = this.getEffectiveBinding();
|
|
146
|
+
const binding = await this.getEffectiveBinding();
|
|
147
|
+
if (!binding.enabled || !this.hasRoomDemand()) return;
|
|
147
148
|
if (this.current?.host.isServing && this.bindingsMatch(this.activeBinding, binding) && this.connectionSettingsRemainUsable(this.activeBinding?.settings)) {
|
|
148
149
|
this.reconcileCurrentSessionPolicy(this.current);
|
|
149
150
|
Logger("P2P replicator is already open.");
|
|
@@ -156,7 +157,7 @@ class P2PRoomSessionOwner {
|
|
|
156
157
|
const operationGeneration = this.runtimeGeneration;
|
|
157
158
|
try {
|
|
158
159
|
const connectionSettings = await this.prepareConnectionSettings(binding, operationGeneration);
|
|
159
|
-
this.assertReconciliationCurrent(binding, operationGeneration, connectionSettings);
|
|
160
|
+
await this.assertReconciliationCurrent(binding, operationGeneration, connectionSettings);
|
|
160
161
|
const connectionBinding = { ...binding, settings: connectionSettings };
|
|
161
162
|
candidate = this.createSession(this.buildSessionEnv(connectionBinding));
|
|
162
163
|
if (hasManagedP2PTurnConfiguration(binding.settings)) {
|
|
@@ -167,7 +168,7 @@ class P2PRoomSessionOwner {
|
|
|
167
168
|
if (!candidate.host.isServing) {
|
|
168
169
|
throw new Error("The P2P room did not start serving.");
|
|
169
170
|
}
|
|
170
|
-
const currentBinding = this.getEffectiveBinding();
|
|
171
|
+
const currentBinding = await this.getEffectiveBinding();
|
|
171
172
|
if (!currentBinding.enabled || !this.hasRoomDemand() || !this.bindingsMatch(binding, currentBinding) || operationGeneration !== this.runtimeGeneration || !this.connectionSettingsRemainUsable(connectionSettings)) {
|
|
172
173
|
await candidate.retire();
|
|
173
174
|
return;
|
|
@@ -243,13 +244,21 @@ class P2PRoomSessionOwner {
|
|
|
243
244
|
if (expiresAt === void 0) return !managed;
|
|
244
245
|
return Number.isFinite(expiresAt) && expiresAt > Date.now() + P2P_ICE_SERVER_EXPIRY_MARGIN_MS;
|
|
245
246
|
}
|
|
246
|
-
fencePreparationForCurrentSettings(enabled) {
|
|
247
|
+
async fencePreparationForCurrentSettings(enabled) {
|
|
247
248
|
if (!enabled) {
|
|
248
249
|
this.invalidatePreparation();
|
|
249
250
|
return;
|
|
250
251
|
}
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
252
|
+
const pending = this.pendingPreparation;
|
|
253
|
+
if (!pending) return;
|
|
254
|
+
if (pending.configurationIdentity !== getP2PReplicatorConfigurationIdentity(this.env.services.setting.currentSettings())) {
|
|
255
|
+
this.invalidatePreparation();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const binding = await this.getEffectiveBinding();
|
|
259
|
+
if (this.pendingPreparation === pending && pending.signature !== binding.signature) {
|
|
260
|
+
this.invalidatePreparation();
|
|
261
|
+
}
|
|
253
262
|
}
|
|
254
263
|
invalidatePreparation() {
|
|
255
264
|
this.runtimeGeneration += 1;
|
|
@@ -269,6 +278,7 @@ class P2PRoomSessionOwner {
|
|
|
269
278
|
const controller = new AbortController();
|
|
270
279
|
const pending = {
|
|
271
280
|
signature: binding.signature,
|
|
281
|
+
configurationIdentity: getP2PReplicatorConfigurationIdentity(binding.settings),
|
|
272
282
|
controller
|
|
273
283
|
};
|
|
274
284
|
this.pendingPreparation = pending;
|
|
@@ -288,7 +298,7 @@ class P2PRoomSessionOwner {
|
|
|
288
298
|
if (!this.connectionSettingsRemainUsable(connectionSettings)) {
|
|
289
299
|
throw new Error("The prepared managed TURN settings are missing usable ICE servers or expiry.");
|
|
290
300
|
}
|
|
291
|
-
this.assertReconciliationCurrent(binding, operationGeneration, connectionSettings);
|
|
301
|
+
await this.assertReconciliationCurrent(binding, operationGeneration, connectionSettings);
|
|
292
302
|
return connectionSettings;
|
|
293
303
|
} finally {
|
|
294
304
|
if (this.pendingPreparation === pending) {
|
|
@@ -359,23 +369,26 @@ class P2PRoomSessionOwner {
|
|
|
359
369
|
if (timeout !== void 0) globalThis.clearTimeout(timeout);
|
|
360
370
|
}
|
|
361
371
|
}
|
|
362
|
-
assertReconciliationCurrent(binding, operationGeneration, connectionSettings) {
|
|
372
|
+
async assertReconciliationCurrent(binding, operationGeneration, connectionSettings) {
|
|
373
|
+
if (operationGeneration !== this.runtimeGeneration || !this.hasRoomDemand()) {
|
|
374
|
+
throw new StaleRoomReconciliationError();
|
|
375
|
+
}
|
|
376
|
+
const currentBinding = await this.getEffectiveBinding();
|
|
363
377
|
if (operationGeneration !== this.runtimeGeneration || !this.hasRoomDemand()) {
|
|
364
378
|
throw new StaleRoomReconciliationError();
|
|
365
379
|
}
|
|
366
|
-
const currentBinding = this.getEffectiveBinding();
|
|
367
380
|
if (!currentBinding.enabled) throw new StaleRoomReconciliationError();
|
|
368
381
|
if (binding && !this.bindingsMatch(binding, currentBinding)) throw new StaleRoomReconciliationError();
|
|
369
382
|
if (connectionSettings && !this.connectionSettingsRemainUsable(connectionSettings)) {
|
|
370
383
|
throw new StaleRoomReconciliationError();
|
|
371
384
|
}
|
|
372
385
|
}
|
|
373
|
-
/**
|
|
374
|
-
getEffectiveBinding() {
|
|
386
|
+
/** Read the device identity before capturing the current synchronous session inputs. */
|
|
387
|
+
async getEffectiveBinding() {
|
|
388
|
+
const deviceName = await this.env.services.config.getValue(SETTING_KEY_P2P_DEVICE_NAME) || this.env.services.vault.getVaultName();
|
|
375
389
|
const settings = omitP2PRuntimeSettings({
|
|
376
390
|
...this.env.services.setting.currentSettings()
|
|
377
391
|
});
|
|
378
|
-
const deviceName = this.env.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) || this.env.services.vault.getVaultName();
|
|
379
392
|
const database = this.env.services.database.localDatabase.localDatabase;
|
|
380
393
|
this.automationCoordinator.reconcileIdentity(
|
|
381
394
|
JSON.stringify([
|
|
@@ -144,6 +144,7 @@ export declare class TrysteroReplicator {
|
|
|
144
144
|
idDerivationVersion: 0 | 1;
|
|
145
145
|
idDerivationProof: string;
|
|
146
146
|
liveSync?: boolean;
|
|
147
|
+
batchSaveInLiveSync?: boolean;
|
|
147
148
|
syncOnSave?: boolean;
|
|
148
149
|
syncOnStart?: boolean;
|
|
149
150
|
syncOnFileOpen?: boolean;
|
|
@@ -332,6 +333,7 @@ export declare class TrysteroReplicator {
|
|
|
332
333
|
idDerivationVersion: 0 | 1;
|
|
333
334
|
idDerivationProof: string;
|
|
334
335
|
liveSync?: boolean;
|
|
336
|
+
batchSaveInLiveSync?: boolean;
|
|
335
337
|
syncOnSave?: boolean;
|
|
336
338
|
syncOnStart?: boolean;
|
|
337
339
|
syncOnFileOpen?: boolean;
|
|
@@ -360,8 +360,7 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
|
|
|
360
360
|
case BASE_IS_NEW:
|
|
361
361
|
if (!host.services.vault.isFileSizeTooLarge(file.stat.size)) {
|
|
362
362
|
log("STORAGE -> DB :" + file.path);
|
|
363
|
-
await host.serviceModules.fileHandler.storeFileToDB(file);
|
|
364
|
-
return FilePairProcessResults.COMPLETED;
|
|
363
|
+
return await host.serviceModules.fileHandler.storeFileToDB(file) ? FilePairProcessResults.COMPLETED : FilePairProcessResults.FAILED;
|
|
365
364
|
} else {
|
|
366
365
|
log(
|
|
367
366
|
`STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`,
|
|
@@ -390,11 +389,22 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
|
|
|
390
389
|
return FilePairProcessResults.SKIPPED;
|
|
391
390
|
}
|
|
392
391
|
case EVEN:
|
|
393
|
-
if (doc.
|
|
394
|
-
|
|
392
|
+
if (host.services.vault.isFileSizeTooLarge(file.stat.size) || host.services.vault.isFileSizeTooLarge(doc.size)) {
|
|
393
|
+
log(`STORAGE ? DB : ${file.path} has been skipped due to file size exceeding the limit`, LOG_LEVEL_NOTICE);
|
|
394
|
+
return FilePairProcessResults.SKIPPED;
|
|
395
|
+
}
|
|
396
|
+
if (doc._rev && await host.serviceModules.fileHandler.tryRecordUntrackedFileRevision(file, doc._rev)) {
|
|
397
|
+
log("STORAGE == DB :" + file.path, LOG_LEVEL_DEBUG);
|
|
398
|
+
return FilePairProcessResults.COMPLETED;
|
|
399
|
+
}
|
|
400
|
+
log("STORAGE ? DB :" + file.path + " Checking content", LOG_LEVEL_DEBUG);
|
|
401
|
+
try {
|
|
402
|
+
return await host.serviceModules.fileHandler.storeFileToDB(file, false, false, { revalidateStorage: true }) ? FilePairProcessResults.COMPLETED : FilePairProcessResults.FAILED;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
log(`STORAGE ? DB : Could not reconcile ${file.path}`, LOG_LEVEL_NOTICE);
|
|
405
|
+
log(error, LOG_LEVEL_VERBOSE);
|
|
406
|
+
return FilePairProcessResults.FAILED;
|
|
395
407
|
}
|
|
396
|
-
log("STORAGE == DB :" + file.path, LOG_LEVEL_DEBUG);
|
|
397
|
-
return FilePairProcessResults.COMPLETED;
|
|
398
408
|
default:
|
|
399
409
|
log("STORAGE ?? DB :" + file.path + " Something got weird");
|
|
400
410
|
return FilePairProcessResults.FAILED;
|
|
@@ -501,7 +511,9 @@ async function collectDatabaseFiles(host, settings, log, showingNotice) {
|
|
|
501
511
|
async function updateToDatabase(host, log, logLevel, file) {
|
|
502
512
|
if (!host.services.vault.isFileSizeTooLarge(file.stat.size)) {
|
|
503
513
|
const path = file.path;
|
|
504
|
-
await host.serviceModules.fileHandler.storeFileToDB(file)
|
|
514
|
+
if (!await host.serviceModules.fileHandler.storeFileToDB(file)) {
|
|
515
|
+
return FilePairProcessResults.FAILED;
|
|
516
|
+
}
|
|
505
517
|
host.services.context.events.emitEvent("event-file-changed", { file: path, automated: true });
|
|
506
518
|
return FilePairProcessResults.COMPLETED;
|
|
507
519
|
} else {
|