@vrtmrz/livesync-commonlib 0.1.26 → 0.1.28

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.
@@ -68,6 +68,18 @@ export declare function runWithInterval<T>(key: string, interval: number, task:
68
68
  export declare function runWithStartInterval<T>(key: string, interval: number, task: () => Promise<T>): Promise<T>;
69
69
  export declare const globalConcurrencyController: import("octagonal-wheels/concurrency/semaphore_v2").SemaphoreObject;
70
70
  export declare function determineTypeFromBlob(data: Blob): "newnote" | "plain";
71
+ /**
72
+ * Whether remediation mode is active.
73
+ *
74
+ * While a modification-time limit is configured, Commonlib refuses every reconciliation scan
75
+ * between the storage and the local database and keeps storage events unqueued, so that a state
76
+ * being recovered is not overwritten by the current one. A host therefore stays unready while the
77
+ * limit is configured, because readiness depends upon that refused scan. Applying the limit to the
78
+ * documents which arrive is the responsibility of the host.
79
+ * @param settings Settings to inspect
80
+ * @returns `true` when a modification-time limit is configured.
81
+ */
82
+ export declare function isRemediationModeActive(settings: Pick<ObsidianLiveSyncSettings, "maxMTimeForReflectEvents">): boolean;
71
83
  export declare function determineType(path: string, data: string | string[] | Uint8Array | ArrayBuffer | Blob): "newnote" | "plain";
72
84
  export declare function isAnyNote(doc: DatabaseEntry): doc is NewEntry | PlainEntry;
73
85
  export declare function isLoadedEntry(doc: DatabaseEntry): doc is LoadedEntry;
@@ -199,6 +199,9 @@ const globalConcurrencyController = Semaphore(50);
199
199
  function determineTypeFromBlob(data) {
200
200
  return isTextBlob(data) ? "plain" : "newnote";
201
201
  }
202
+ function isRemediationModeActive(settings) {
203
+ return (settings.maxMTimeForReflectEvents ?? 0) > 0;
204
+ }
202
205
  function determineType(path, data) {
203
206
  if (data instanceof Blob) {
204
207
  return determineTypeFromBlob(data);
@@ -548,6 +551,7 @@ export {
548
551
  isObfuscatedEntry,
549
552
  isObjectDifferent,
550
553
  isObjectMargeApplicable,
554
+ isRemediationModeActive,
551
555
  isSensibleMargeApplicable,
552
556
  isSyncInfoEntry,
553
557
  isTextBlob,
@@ -2,6 +2,12 @@ import type { FilePath, FilePathWithPrefix, MetaEntry } from "../common/models/d
2
2
  import type { UXFileInfo, UXFileInfoStub, UXInternalFileInfoStub } from "../common/models/fileaccess.type.js";
3
3
  export interface IFileHandler {
4
4
  readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise<UXFileInfo>;
5
+ /**
6
+ * Record an untracked, unchanged file during an equal-time scan.
7
+ * Return true only when the exact current revision was recorded. This
8
+ * operation never stores a document revision or writes to storage.
9
+ */
10
+ tryRecordUntrackedFileRevision(info: UXFileInfoStub | UXFileInfo, expectedRevision: string): Promise<boolean>;
5
11
  storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean): Promise<boolean>;
6
12
  /**
7
13
  * Store the current storage content as a child of an exact live database revision.
@@ -209,7 +209,7 @@ function isTargetFile(host, filenameSrc) {
209
209
  const file = filenameSrc.startsWith(ICHeader) ? filenameSrc.substring(ICHeader.length) : filenameSrc;
210
210
  if (file.startsWith(ICXHeader)) return true;
211
211
  if (file.startsWith(PSCHeader)) return true;
212
- if (file.includes(":")) {
212
+ if (file.startsWith(IDPrefixes.Chunk) || file.startsWith(IDPrefixes.Obfuscated)) {
213
213
  return false;
214
214
  }
215
215
  if (!settings.syncInternalFiles) {
@@ -6,7 +6,7 @@ import {
6
6
  LOG_LEVEL_NOTICE,
7
7
  LOG_LEVEL_VERBOSE
8
8
  } from "../common/types.js";
9
- import { delay, fireAndForget, throttle } from "../common/utils.js";
9
+ import { delay, fireAndForget, isRemediationModeActive, throttle } from "../common/utils.js";
10
10
  import {} from "../common/types.js";
11
11
  import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
12
12
  import { isWaitingForTimeout } from "octagonal-wheels/concurrency/task";
@@ -86,7 +86,7 @@ class StorageEventManagerBase extends StorageEventManager {
86
86
  const settings = this.settings;
87
87
  if (!settings.isConfigured) return;
88
88
  if (settings.suspendFileWatching) return;
89
- if (settings.maxMTimeForReflectEvents > 0) {
89
+ if (isRemediationModeActive(settings)) {
90
90
  return;
91
91
  }
92
92
  this.fileProcessing.onStorageFileEvent();
@@ -131,12 +131,16 @@ export type ReplicationFailed = {
131
131
  export type ReplicationOutcome = ReplicationCompleted | ReplicationCancelled | ReplicationBlocked | ReplicationPartial | ReplicationFailed;
132
132
  /** Preparation required before an ordinary operation may use a provider. */
133
133
  export interface ReplicationReadinessRequirements {
134
- readonly centralRemotePreparation: "required" | "not-applicable";
134
+ readonly centralRemotePreparation: "required" | "provider-owned" | "not-applicable";
135
135
  }
136
136
  /** Shared readiness contract for providers backed by the central remote. */
137
137
  export declare const CENTRAL_REMOTE_REPLICATION_READINESS: Readonly<{
138
138
  readonly centralRemotePreparation: "required";
139
139
  }>;
140
+ /** Central provider which prepares its Security Seed within each transfer. */
141
+ export declare const PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS: Readonly<{
142
+ readonly centralRemotePreparation: "provider-owned";
143
+ }>;
140
144
  /** Shared readiness contract for peer-to-peer providers without a central remote. */
141
145
  export declare const PEER_REPLICATION_READINESS: Readonly<{
142
146
  readonly centralRemotePreparation: "not-applicable";
@@ -25,6 +25,9 @@ const REPLICATION_CANCELLED = Object.freeze({ status: "cancelled" });
25
25
  const CENTRAL_REMOTE_REPLICATION_READINESS = Object.freeze({
26
26
  centralRemotePreparation: "required"
27
27
  });
28
+ const PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS = Object.freeze({
29
+ centralRemotePreparation: "provider-owned"
30
+ });
28
31
  const PEER_REPLICATION_READINESS = Object.freeze({
29
32
  centralRemotePreparation: "not-applicable"
30
33
  });
@@ -131,6 +134,7 @@ export {
131
134
  CENTRAL_REMOTE_REPLICATION_READINESS,
132
135
  NO_INTERACTION,
133
136
  PEER_REPLICATION_READINESS,
137
+ PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS,
134
138
  REPLICATION_CANCELLED,
135
139
  REPLICATION_COMPLETED,
136
140
  REPLICATION_PROGRESS_PRESENTATIONS,
@@ -32,4 +32,6 @@ export declare class SyncParamsNotFoundError extends SyncParamsHandlerError {
32
32
  }
33
33
  export declare class SyncParamsUpdateError extends SyncParamsHandlerError {
34
34
  }
35
+ /** Create a handler owned by one operation rather than the process-wide server cache. */
36
+ export declare function createSyncParamsHandler({ put, get, create }: CreateSyncParamsHanderOptions): SyncParamsHandler;
35
37
  export {};
@@ -114,5 +114,6 @@ export {
114
114
  SyncParamsNotFoundError,
115
115
  SyncParamsUpdateError,
116
116
  clearHandlers,
117
- createSyncParamsHanderForServer
117
+ createSyncParamsHanderForServer,
118
+ createSyncParamsHandler
118
119
  };
@@ -1,4 +1,4 @@
1
- export { CAPABILITY_NOT_APPLICABLE, CAPABILITY_NOT_IMPLEMENTED, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, PEER_REPLICATION_READINESS, REPLICATION_CANCELLED, REPLICATION_COMPLETED, REPLICATION_PROGRESS_PRESENTATIONS, USER_INITIATED_REPLICATION_AUTHORITY, defineReplicatorProviderDefinitions, isReplicationCompleted, outcomeFromContinuousOpenReplication, outcomeFromFiniteOpenReplication, replicationBlocked, replicationFailed, supportedOpenReplicationContinuous, supportedOpenReplicationOneShot, supportedOpenReplicationUnattended, supportedStopActiveTransfer, supportedCapability, } from "./ReplicatorProvider.js";
1
+ export { CAPABILITY_NOT_APPLICABLE, CAPABILITY_NOT_IMPLEMENTED, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, PEER_REPLICATION_READINESS, PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS, REPLICATION_CANCELLED, REPLICATION_COMPLETED, REPLICATION_PROGRESS_PRESENTATIONS, USER_INITIATED_REPLICATION_AUTHORITY, defineReplicatorProviderDefinitions, isReplicationCompleted, outcomeFromContinuousOpenReplication, outcomeFromFiniteOpenReplication, replicationBlocked, replicationFailed, supportedOpenReplicationContinuous, supportedOpenReplicationOneShot, supportedOpenReplicationUnattended, supportedStopActiveTransfer, supportedCapability, } from "./ReplicatorProvider.js";
2
2
  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";
@@ -4,6 +4,7 @@ import {
4
4
  CENTRAL_REMOTE_REPLICATION_READINESS,
5
5
  NO_INTERACTION,
6
6
  PEER_REPLICATION_READINESS,
7
+ PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS,
7
8
  REPLICATION_CANCELLED,
8
9
  REPLICATION_COMPLETED,
9
10
  REPLICATION_PROGRESS_PRESENTATIONS,
@@ -48,6 +49,7 @@ export {
48
49
  NO_INTERACTION,
49
50
  NO_REMOTE_RESOURCE_CAPABILITIES,
50
51
  PEER_REPLICATION_READINESS,
52
+ PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS,
51
53
  REMOTE_RESOURCE_KINDS,
52
54
  REPLICATION_CANCELLED,
53
55
  REPLICATION_COMPLETED,
@@ -16,6 +16,7 @@ export declare class JournalSyncCore {
16
16
  env: LiveSyncJournalReplicatorEnv;
17
17
  store: SimpleStore<CheckPointInfo>;
18
18
  requestedStop: boolean;
19
+ private syncParamsHandler?;
19
20
  getInitialSyncParameters(): Promise<SyncParameters>;
20
21
  getSyncParameters(): Promise<SyncParameters>;
21
22
  putSyncParameters(params: SyncParameters): Promise<boolean>;
@@ -44,6 +45,7 @@ export declare class JournalSyncCore {
44
45
  resetBucket(): Promise<boolean>;
45
46
  getRemoteKey(): string;
46
47
  getReplicationPBKDF2Salt(refresh?: boolean): Promise<Uint8Array<ArrayBuffer>>;
48
+ private getSyncParamsHandler;
47
49
  isEncryptionPrevented(fileName: string): boolean;
48
50
  private decryptDataV2;
49
51
  private decryptDataV1;
@@ -22,8 +22,7 @@ import {
22
22
  JournalStorageReadStatuses
23
23
  } from "./objectstore/JournalStorageAdapter.js";
24
24
  import {
25
- clearHandlers,
26
- createSyncParamsHanderForServer,
25
+ createSyncParamsHandler,
27
26
  SyncParamsFetchError,
28
27
  SyncParamsNotFoundError,
29
28
  SyncParamsUpdateError
@@ -61,6 +60,7 @@ class JournalSyncCore {
61
60
  env;
62
61
  store;
63
62
  requestedStop = false;
63
+ syncParamsHandler;
64
64
  getInitialSyncParameters() {
65
65
  return Promise.resolve({
66
66
  ...DEFAULT_SYNC_PARAMETERS,
@@ -109,7 +109,6 @@ class JournalSyncCore {
109
109
  this.store = store;
110
110
  this.hash = this.getHash(settings);
111
111
  this.storage = storage;
112
- clearHandlers();
113
112
  }
114
113
  async downloadJson(key) {
115
114
  try {
@@ -156,7 +155,7 @@ class JournalSyncCore {
156
155
  this.store = store;
157
156
  this.hash = this.getHash(settings);
158
157
  this.storage.applyNewConfig(settings);
159
- clearHandlers();
158
+ this.resetAllCaches();
160
159
  }
161
160
  updateInfo(info) {
162
161
  const old = this.env.services.replicator.replicationStatics.value;
@@ -205,23 +204,18 @@ class JournalSyncCore {
205
204
  return this._currentCheckPointInfo;
206
205
  }
207
206
  resetAllCaches() {
208
- clearHandlers();
207
+ this.syncParamsHandler = void 0;
209
208
  }
210
209
  async resetCheckpointInfo() {
211
210
  await this.updateCheckPointInfo((info) => ({ ...CheckPointInfoDefault }));
212
- clearHandlers();
213
211
  }
214
212
  getJournalEpochFromSyncParams(params) {
215
213
  return `${params.protocolVersion}:${params.pbkdf2salt}`;
216
214
  }
217
215
  async ensureCheckpointCachesAreFresh() {
218
- let journalEpoch = "";
219
- try {
220
- const params = await this.getSyncParameters();
221
- journalEpoch = this.getJournalEpochFromSyncParams(params);
222
- } catch {
223
- return;
224
- }
216
+ const params = await this.getSyncParamsHandler().fetch(true);
217
+ if (!params) throw new SyncParamsFetchError("Could not prepare Journal synchronisation parameters");
218
+ const journalEpoch = this.getJournalEpochFromSyncParams(params);
225
219
  const current = await this.getCheckpointInfo();
226
220
  if (current.journalEpoch === journalEpoch) {
227
221
  return;
@@ -252,12 +246,12 @@ class JournalSyncCore {
252
246
  receivedFiles: /* @__PURE__ */ new Set(),
253
247
  sentFiles: /* @__PURE__ */ new Set()
254
248
  }));
255
- clearHandlers();
256
249
  }
257
250
  async isAvailable() {
258
251
  return await this.storage.isAvailable();
259
252
  }
260
253
  async resetBucket() {
254
+ this.resetAllCaches();
261
255
  let files = [];
262
256
  try {
263
257
  do {
@@ -269,7 +263,6 @@ class JournalSyncCore {
269
263
  return false;
270
264
  }
271
265
  } while (files.length != 0);
272
- clearHandlers();
273
266
  } catch (ex) {
274
267
  Logger(`WARNING! Could not delete files.`, LOG_LEVEL_NOTICE, "reset-bucket");
275
268
  Logger(ex, LOG_LEVEL_VERBOSE);
@@ -291,13 +284,14 @@ class JournalSyncCore {
291
284
  return this.getHash(this._settings);
292
285
  }
293
286
  async getReplicationPBKDF2Salt(refresh) {
294
- const server = this.getRemoteKey();
295
- const manager = createSyncParamsHanderForServer(server, {
287
+ return await this.getSyncParamsHandler().getPBKDF2Salt(refresh);
288
+ }
289
+ getSyncParamsHandler() {
290
+ return this.syncParamsHandler ??= createSyncParamsHandler({
296
291
  put: (params) => this.putSyncParameters(params),
297
292
  get: () => this.getSyncParameters(),
298
293
  create: () => this.getInitialSyncParameters()
299
294
  });
300
- return await manager.getPBKDF2Salt(refresh);
301
295
  }
302
296
  isEncryptionPrevented(fileName) {
303
297
  if (fileName.endsWith(DOCID_JOURNAL_SYNC_PARAMETERS)) return true;
@@ -762,6 +756,7 @@ class JournalSyncCore {
762
756
  /** Stop this Journal operation owner and release its storage transport. */
763
757
  dispose() {
764
758
  this.requestStop();
759
+ this.resetAllCaches();
765
760
  this.storage.dispose?.();
766
761
  }
767
762
  }
@@ -12,7 +12,6 @@ import { MinioStorageAdapter } from "./objectstore/MinioStorageAdapter.js";
12
12
  import { LiveSyncAbstractReplicator } from "../LiveSyncAbstractReplicator.js";
13
13
  import { ensureRemoteIsCompatible } from "../../pouchdb/LiveSyncDBFunctions.js";
14
14
  import { extractObject } from "../../common/utils.js";
15
- import { clearHandlers } from "../SyncParamsHandler.js";
16
15
  import { JournalStorageReadStatuses } from "./objectstore/JournalStorageAdapter.js";
17
16
  import {
18
17
  CENTRAL_COMPATIBILITY_ACCEPTED,
@@ -154,20 +153,24 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
154
153
  async openReplication(setting, _, showResult, ignoreCleanLock = false, recordCompatibilityDecision) {
155
154
  return await this.runJournalTransfer(async (stopGeneration) => {
156
155
  const client = this.setupJournalSyncClient(setting);
157
- stopGeneration = this.journalTransferStopGeneration;
158
- if (!await this.checkReplicationConnectivity(
159
- false,
160
- ignoreCleanLock,
161
- showResult,
162
- setting,
163
- client,
164
- recordCompatibilityDecision
165
- )) {
166
- return false;
156
+ try {
157
+ stopGeneration = this.journalTransferStopGeneration;
158
+ if (!await this.checkReplicationConnectivity(
159
+ false,
160
+ ignoreCleanLock,
161
+ showResult,
162
+ setting,
163
+ client,
164
+ recordCompatibilityDecision
165
+ )) {
166
+ return false;
167
+ }
168
+ if (stopGeneration !== this.journalTransferStopGeneration) return false;
169
+ this.hasEnteredReplication = true;
170
+ return await client.sync(showResult);
171
+ } finally {
172
+ client.resetAllCaches();
167
173
  }
168
- if (stopGeneration !== this.journalTransferStopGeneration) return false;
169
- this.hasEnteredReplication = true;
170
- return await client.sync(showResult);
171
174
  });
172
175
  }
173
176
  /**
@@ -191,20 +194,24 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
191
194
  async runDirectionalReplication(setting, showingNotice, transfer, recordCompatibilityDecision) {
192
195
  return await this.runJournalTransfer(async (stopGeneration) => {
193
196
  const client = this.setupJournalSyncClient(setting);
194
- stopGeneration = this.journalTransferStopGeneration;
195
- if (!await this.checkReplicationConnectivity(
196
- false,
197
- false,
198
- !!showingNotice,
199
- setting,
200
- client,
201
- recordCompatibilityDecision
202
- )) {
203
- return false;
197
+ try {
198
+ stopGeneration = this.journalTransferStopGeneration;
199
+ if (!await this.checkReplicationConnectivity(
200
+ false,
201
+ false,
202
+ !!showingNotice,
203
+ setting,
204
+ client,
205
+ recordCompatibilityDecision
206
+ )) {
207
+ return false;
208
+ }
209
+ if (stopGeneration !== this.journalTransferStopGeneration) return false;
210
+ this.hasEnteredReplication = true;
211
+ return await transfer(client);
212
+ } finally {
213
+ client.resetAllCaches();
204
214
  }
205
- if (stopGeneration !== this.journalTransferStopGeneration) return false;
206
- this.hasEnteredReplication = true;
207
- return await transfer(client);
208
215
  });
209
216
  }
210
217
  /** Capture only the compatibility decision made by this exact borrowed client. */
@@ -344,7 +351,6 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
344
351
  if (!await this.client.resetBucket()) {
345
352
  throw new Error("Could not reset remote bucket");
346
353
  }
347
- clearHandlers();
348
354
  Logger("Remote Bucket Cleared", LOG_LEVEL_NOTICE);
349
355
  await this.tryCreateRemoteDatabase(setting);
350
356
  } catch (ex) {
@@ -356,7 +362,6 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
356
362
  async tryCreateRemoteDatabase(setting) {
357
363
  this.closeReplication();
358
364
  Logger("Remote Database Created or Connected", LOG_LEVEL_NOTICE);
359
- clearHandlers();
360
365
  if (!await this.ensurePBKDF2Salt(setting, true, false)) {
361
366
  throw new Error("Could not ensure PBKDF2 salt (Security Seed)");
362
367
  }
@@ -7,7 +7,7 @@ import {
7
7
  LOG_LEVEL_VERBOSE,
8
8
  isMetaEntry
9
9
  } from "../common/types.js";
10
- import { compareMTime, isAnyNote } from "../common/utils.js";
10
+ import { compareMTime, isAnyNote, isRemediationModeActive } from "../common/utils.js";
11
11
  import { shouldBeIgnored, stripAllPrefixes } from "../string_and_binary/path.js";
12
12
  import { createInstanceLogFunction } from "../services/lib/logUtils.js";
13
13
  import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../common/models/shared.const.symbols.js";
@@ -389,6 +389,9 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
389
389
  return FilePairProcessResults.SKIPPED;
390
390
  }
391
391
  case EVEN:
392
+ if (doc._rev) {
393
+ await host.serviceModules.fileHandler.tryRecordUntrackedFileRevision(file, doc._rev);
394
+ }
392
395
  log("STORAGE == DB :" + file.path, LOG_LEVEL_DEBUG);
393
396
  return FilePairProcessResults.COMPLETED;
394
397
  default:
@@ -411,7 +414,7 @@ function canProceedScan(host, errorManager, log, showingNotice = false, ignoreSu
411
414
  }
412
415
  errorManager.clearError(ERR_SUSPENDING);
413
416
  const MSG_IN_REMEDIATION = `Started in remediation Mode! (Max mtime for reflect events is set). Synchronising between the storage and the local database is now prevented.`;
414
- if (settings.maxMTimeForReflectEvents > 0) {
417
+ if (isRemediationModeActive(settings)) {
415
418
  errorManager.showError(MSG_IN_REMEDIATION, LOG_LEVEL_NOTICE);
416
419
  return false;
417
420
  }
@@ -8,7 +8,7 @@ import { ServiceModuleBase } from "./ServiceModuleBase.js";
8
8
  import { fetchChangesForInitialSync, isRetryableStreamingFetchFailure } from "../pouchdb/StreamingFetch.js";
9
9
  import { getConfiguredFunctionsForEncryption } from "../pouchdb/encryption.js";
10
10
  import { AuthorizationHeaderGenerator, generateCredentialObject } from "../replication/httplib.js";
11
- import { parseHeaderValues } from "../common/utils.js";
11
+ import { isRemediationModeActive, parseHeaderValues } from "../common/utils.js";
12
12
  import { sizeToHumanReadable } from "octagonal-wheels/number";
13
13
  import { REMOTE_RESOURCE_KINDS } from "../replication/index.js";
14
14
  const FAST_FETCH_CHECKPOINT_KEY = "fast-fetch-checkpoint";
@@ -106,6 +106,15 @@ Please enable them from the settings screen after setup is complete.`,
106
106
  await this.informOptionalFeatures();
107
107
  }
108
108
  async performRebuildEverything() {
109
+ if (isRemediationModeActive(this.setting.currentSettings())) {
110
+ this._log(
111
+ `Rebuilding has been refused: remediation mode is active. Clear the modification-time limit to rebuild.`,
112
+ LOG_LEVEL_NOTICE
113
+ );
114
+ throw new Error(
115
+ "Rebuilding is not available while remediation mode is active. Clear the modification-time limit first."
116
+ );
117
+ }
109
118
  this.appLifecycle.resetIsReady();
110
119
  await this.setting.suspendExtraSync();
111
120
  await this.setting.applyPartial({
@@ -249,7 +258,8 @@ Please enable them from the settings screen after setup is complete.`,
249
258
  suspendParseReplicationResult: false,
250
259
  suspendFileWatching: false
251
260
  });
252
- if (!await this.vault.scanVault(true)) return false;
261
+ const inRemediationMode = isRemediationModeActive(this.setting.currentSettings());
262
+ if (!inRemediationMode && !await this.vault.scanVault(true)) return false;
253
263
  if (!await this.replication.onBeforeReplicate(false)) return false;
254
264
  return true;
255
265
  }
@@ -281,7 +291,7 @@ Please enable them from the settings screen after setup is complete.`,
281
291
  notifyThresholdOfRemoteStorageSize: DEFAULT_SETTINGS.notifyThresholdOfRemoteStorageSize
282
292
  });
283
293
  const settings = this.setting.currentSettings();
284
- if (settings.maxMTimeForReflectEvents > 0) {
294
+ if (isRemediationModeActive(settings)) {
285
295
  const date = new Date(settings.maxMTimeForReflectEvents);
286
296
  const ask = `Your settings restrict file reflection times to no later than ${date.toLocaleString()}.
287
297
 
@@ -318,7 +328,12 @@ Are you sure you wish to proceed?`;
318
328
  await this.resetLocalDatabase();
319
329
  this.clearFastFetchCheckpoint();
320
330
  await delay(1e3);
321
- if (makeLocalChunkBeforeSync) {
331
+ if (isRemediationModeActive(this.setting.currentSettings())) {
332
+ this._log(
333
+ `Remediation mode: the files in storage are not stored in the database before fetching.`,
334
+ LOG_LEVEL_NOTICE
335
+ );
336
+ } else if (makeLocalChunkBeforeSync) {
322
337
  await this.fileHandler.createAllChunks(true);
323
338
  } else if (!preventMakeLocalFilesBeforeSync) {
324
339
  await this.prepareLocalDatabaseForRebuild();
@@ -496,7 +511,9 @@ Total bytes fetched: ${sizeToHumanReadable(progress.totalBytes)}`,
496
511
  if (controlsReflection) {
497
512
  await this.setting.saveSettingData();
498
513
  }
499
- this.appLifecycle.markIsReady();
514
+ if (!isRemediationModeActive(this.setting.currentSettings())) {
515
+ this.appLifecycle.markIsReady();
516
+ }
500
517
  completed = true;
501
518
  return true;
502
519
  } finally {
@@ -94,6 +94,7 @@ export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase<S
94
94
  getPath(entry: AnyEntry): FilePathWithPrefix;
95
95
  getPathWithoutPrefix(entry: AnyEntry): FilePathWithPrefix;
96
96
  readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise<UXFileInfo>;
97
+ tryRecordUntrackedFileRevision(info: UXFileInfoStub | UXFileInfo, expectedRevision: string): Promise<boolean>;
97
98
  private infoToStub;
98
99
  storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean): Promise<boolean>;
99
100
  storeFileToDBWithBaseRevision(info: UXFileInfoStub | UXFileInfo | FilePathWithPrefix, baseRevision: string, createIfDifferent?: boolean): Promise<boolean>;
@@ -215,6 +215,36 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
215
215
  }
216
216
  return readFile;
217
217
  }
218
+ async tryRecordUntrackedFileRevision(info, expectedRevision) {
219
+ if (!this.fileReflectionProvenance || !expectedRevision) return false;
220
+ const path = getStoragePathFromUXFileInfo(info);
221
+ try {
222
+ return await this.serializedByFileEventPaths([path], async () => {
223
+ const provenance = this.fileReflectionProvenance;
224
+ if (await provenance.get(path)) return false;
225
+ const stub = await this.storage.getStub(path);
226
+ if (!stub || isFolderInfo(stub) || stub.path !== path) return false;
227
+ const file = await this.readFileFromStub(stub);
228
+ const current = await this.db.fetchEntryMeta(path, void 0, true);
229
+ if (!current || current._rev !== expectedRevision || current.deleted || current._deleted) return false;
230
+ if ((await this.db.getConflictedRevs(path)).length > 0) return false;
231
+ const entry = await this.db.fetchEntry(path, expectedRevision, true, true, true);
232
+ if (!entry || entry._rev !== expectedRevision || entry.deleted || entry._deleted) return false;
233
+ if (!await isDocContentSame(readContent(entry), file.body)) return false;
234
+ const latest = await this.db.fetchEntryMeta(path, void 0, true);
235
+ if (!latest || latest._rev !== expectedRevision || latest.deleted || latest._deleted) return false;
236
+ if ((await this.db.getConflictedRevs(path)).length > 0) return false;
237
+ if (!await this.currentStorageSnapshotMatches(file, void 0)) return false;
238
+ if (await provenance.get(path)) return false;
239
+ await provenance.set(path, { revision: expectedRevision, observedStorageMtime: file.stat.mtime });
240
+ return true;
241
+ });
242
+ } catch (ex) {
243
+ this._log(`Could not record unchanged file provenance for ${path}`, LOG_LEVEL_VERBOSE);
244
+ this._log(ex, LOG_LEVEL_VERBOSE);
245
+ return false;
246
+ }
247
+ }
218
248
  async infoToStub(info) {
219
249
  if (info == null) return null;
220
250
  const file = typeof info === "string" ? await this.storage.getFileStub(info) : info;
@@ -1,5 +1,5 @@
1
1
  import { ServiceBase } from "./ServiceBase.js";
2
- import { addPrefix, id2path_base, path2id_base } from "../../string_and_binary/path.js";
2
+ import { addPrefix, expandFilePathPrefix, id2path_base, path2id_base } from "../../string_and_binary/path.js";
3
3
  import { isInternalMetadata, stripInternalMetadataPrefix } from "../../common/typeUtils.js";
4
4
  class PathService extends ServiceBase {
5
5
  settingService;
@@ -14,19 +14,12 @@ class PathService extends ServiceBase {
14
14
  }
15
15
  _id2path(id, entry) {
16
16
  const filename = id2path_base(id, entry);
17
- const temp = filename.split(":");
18
- const path = temp.pop();
19
- const normalizedPath = this.normalizePath(path);
20
- temp.push(normalizedPath);
21
- const fixedPath = temp.join(":");
22
- return fixedPath;
17
+ const [prefix, path] = expandFilePathPrefix(filename);
18
+ return prefix + this.normalizePath(path);
23
19
  }
24
20
  async _path2id(filename, obfuscatePassphrase, caseInsensitive) {
25
- const temp = filename.split(":");
26
- const path = temp.pop();
27
- const normalizedPath = this.normalizePath(path);
28
- temp.push(normalizedPath);
29
- const fixedPath = temp.join(":");
21
+ const [prefix, path] = expandFilePathPrefix(filename);
22
+ const fixedPath = prefix + this.normalizePath(path);
30
23
  const out = await path2id_base(fixedPath, obfuscatePassphrase, caseInsensitive);
31
24
  return out;
32
25
  }
@@ -13,6 +13,7 @@ import {
13
13
  import { memorizeFuncWithLRUCache } from "../common/utils.js";
14
14
  import { uint8ArrayToHexString, writeString } from "./convert.js";
15
15
  import { unique } from "octagonal-wheels/collection.js";
16
+ import { CHeader, ICHeader, ICXHeader, PSCHeader } from "../common/models/fileaccess.const.js";
16
17
  function isValidFilenameInWidows(filename) {
17
18
  const regex = /[\u0000-\u001f]|[\\":?<>|*#]/g;
18
19
  if (regex.test(filename)) return false;
@@ -21,11 +22,11 @@ function isValidFilenameInWidows(filename) {
21
22
  return true;
22
23
  }
23
24
  function isValidFilenameInDarwin(filename) {
24
- const regex = /[\u0000-\u001f]|[:]/g;
25
+ const regex = /[\u0000-\u001f]/g;
25
26
  return !regex.test(filename);
26
27
  }
27
28
  function isValidFilenameInLinux(filename) {
28
- const regex = /[\u0000-\u001f]|[:]/g;
29
+ const regex = /[\u0000-\u001f]/g;
29
30
  return !regex.test(filename);
30
31
  }
31
32
  function isValidFilenameInAndroid(filename) {
@@ -33,37 +34,30 @@ function isValidFilenameInAndroid(filename) {
33
34
  return !regex.test(filename);
34
35
  }
35
36
  function isFilePath(path) {
36
- if (path.indexOf(":") === -1) return true;
37
- return false;
37
+ return expandFilePathPrefix(path)[0] === "";
38
38
  }
39
39
  function stripAllPrefixes(prefixedPath) {
40
- if (isFilePath(prefixedPath)) return prefixedPath;
41
- const [, body] = expandFilePathPrefix(prefixedPath);
42
- return stripAllPrefixes(body);
40
+ return stripPrefix(prefixedPath);
43
41
  }
44
42
  function addPrefix(path, prefix) {
45
43
  if (prefix && path.startsWith(prefix)) return path;
46
44
  return `${prefix ?? ""}${path}`;
47
45
  }
48
46
  function expandFilePathPrefix(path) {
49
- let [prefix, body] = path.split(":", 2);
50
- if (!body) {
51
- body = prefix;
52
- prefix = "";
53
- } else {
54
- prefix = prefix + ":";
47
+ for (const prefix of [ICHeader, ICXHeader, PSCHeader]) {
48
+ if (path.startsWith(prefix)) {
49
+ return [prefix, path.substring(prefix.length)];
50
+ }
55
51
  }
56
- return [prefix, body];
52
+ return ["", path];
57
53
  }
58
54
  function expandDocumentIDPrefix(id) {
59
- let [prefix, body] = id.split(":", 2);
60
- if (!body) {
61
- body = prefix;
62
- prefix = "";
63
- } else {
64
- prefix = prefix + ":";
55
+ for (const prefix of [CHeader, PREFIX_OBFUSCATED]) {
56
+ if (id.startsWith(prefix)) {
57
+ return [prefix, id.substring(prefix.length)];
58
+ }
65
59
  }
66
- return [prefix, body];
60
+ return expandFilePathPrefix(id);
67
61
  }
68
62
  const _hashString = memorizeFuncWithLRUCache(async (key) => {
69
63
  const buff = writeString(key);
@@ -112,11 +106,7 @@ function getPathWithoutPrefix(entry) {
112
106
  return stripAllPrefixes(f);
113
107
  }
114
108
  function stripPrefix(prefixedPath) {
115
- const [prefix, body] = prefixedPath.split(":", 2);
116
- if (!body) {
117
- return prefix;
118
- }
119
- return body;
109
+ return expandFilePathPrefix(prefixedPath)[1];
120
110
  }
121
111
  function shouldBeIgnored(filename) {
122
112
  if (filename == FLAGMD_REDFLAG) {