@vrtmrz/livesync-commonlib 0.1.41 → 0.1.42
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 +7 -1
- package/dist/API/DirectFileManipulatorV2.js +16 -1
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/API/processSetting.js +6 -1
- package/dist/common/fileContentHash.d.ts +2 -0
- package/dist/common/fileContentHash.js +18 -0
- package/dist/common/models/setting.const.defaults.js +1 -0
- 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 +8 -1
- package/dist/common/models/shared.definition.configNames.js +5 -0
- package/dist/common/models/tweak.definition.d.ts +6 -0
- package/dist/common/models/tweak.definition.js +1 -0
- package/dist/common/pathIdentity.d.ts +10 -0
- package/dist/common/pathIdentity.js +34 -0
- package/dist/common/pathNormalization.d.ts +7 -0
- package/dist/common/pathNormalization.js +13 -0
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/hashing.d.ts +1 -0
- package/dist/hashing.js +3 -1
- package/dist/interfaces/FileReflectionProvenance.d.ts +4 -0
- package/dist/managers/EntryManager/EntryManager.d.ts +1 -0
- package/dist/managers/EntryManager/EntryManager.js +9 -0
- package/dist/pouchdb/StreamingFetch.d.ts +3 -1
- package/dist/pouchdb/StreamingFetch.js +3 -1
- package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
- package/dist/pouchdb/pathIdentityCompatibility.js +64 -0
- package/dist/pouchdb/remoteFeatureCompatibility.d.ts +2 -0
- package/dist/pouchdb/remoteFeatureCompatibility.js +9 -1
- package/dist/replication/couchdb/LiveSyncReplicator.js +11 -0
- package/dist/replication/index.d.ts +1 -1
- package/dist/replication/index.js +2 -0
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +2 -0
- package/dist/replication/journal/LiveSyncJournalReplicator.js +28 -6
- package/dist/replication/trystero/P2PRoomSessionOwner.js +18 -5
- package/dist/replication/trystero/TrysteroReplicator.d.ts +2 -0
- package/dist/replication/trystero/useP2PReplicatorFeature.js +2 -1
- package/dist/serviceFeatures/offlineScanner.d.ts +1 -1
- package/dist/serviceFeatures/offlineScanner.js +4 -2
- package/dist/serviceFeatures/targetFilter.d.ts +1 -1
- package/dist/serviceFeatures/targetFilter.js +28 -17
- package/dist/serviceModules/Rebuilder.d.ts +5 -1
- package/dist/serviceModules/Rebuilder.js +85 -3
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +5 -2
- package/dist/serviceModules/ServiceFileHandlerBase.js +110 -49
- package/dist/services/base/IService.d.ts +2 -1
- package/dist/services/base/PathService.d.ts +3 -3
- package/dist/services/base/PathService.js +6 -3
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +3 -0
- package/dist/worker/bgWorker.js +1 -1
- package/docs/conflict-resolution.md +17 -8
- package/docs/proven-in-use.md +7 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
readAsBlob,
|
|
7
7
|
readContent
|
|
8
8
|
} from "../common/utils.js";
|
|
9
|
+
import { computeFileContentHash } from "../common/fileContentHash.js";
|
|
9
10
|
import { EVENT_CONFLICT_CANCELLED } from "../events/coreEvents.js";
|
|
10
11
|
import { shouldBeIgnored, stripAllPrefixes } from "../string_and_binary/path.js";
|
|
11
12
|
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
|
@@ -13,6 +14,7 @@ import { ServiceModuleBase } from "./ServiceModuleBase.js";
|
|
|
13
14
|
import { getStoragePathFromUXFileInfo } from "../common/typeUtils.js";
|
|
14
15
|
import { EVEN } from "../common/models/shared.const.symbols.js";
|
|
15
16
|
import { tryGetFilePath } from "../common/utils.doc.js";
|
|
17
|
+
import { getPathNormalizationVersion, normalisePathForIdentity } from "../common/pathNormalization.js";
|
|
16
18
|
async function serializedByKeys(keys, callback) {
|
|
17
19
|
const [key, ...remainingKeys] = keys;
|
|
18
20
|
if (key === void 0) return await callback();
|
|
@@ -25,6 +27,12 @@ function getParentPath(path) {
|
|
|
25
27
|
function isFolderInfo(info) {
|
|
26
28
|
return info?.isFolder === true;
|
|
27
29
|
}
|
|
30
|
+
function hasContentHash(record) {
|
|
31
|
+
return !!record && typeof record.contentHash === "string" && /^sha256:[0-9a-f]{64}$/.test(record.contentHash) && Number.isSafeInteger(record.contentSize) && record.contentSize >= 0;
|
|
32
|
+
}
|
|
33
|
+
function sameProvenance(left, right) {
|
|
34
|
+
return left?.revision === right?.revision && left?.observedStorageMtime === right?.observedStorageMtime && left?.contentHash === right?.contentHash && left?.contentSize === right?.contentSize;
|
|
35
|
+
}
|
|
28
36
|
class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
29
37
|
events;
|
|
30
38
|
databaseFileAccess;
|
|
@@ -34,6 +42,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
34
42
|
setting;
|
|
35
43
|
vault;
|
|
36
44
|
fileReflectionProvenance;
|
|
45
|
+
contentHashes = /* @__PURE__ */ new WeakMap();
|
|
37
46
|
deferredConflictChecks = /* @__PURE__ */ new Map();
|
|
38
47
|
constructor(services) {
|
|
39
48
|
super(services);
|
|
@@ -54,28 +63,51 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
54
63
|
get storage() {
|
|
55
64
|
return this.storageAccess;
|
|
56
65
|
}
|
|
66
|
+
getProvenancePath(path) {
|
|
67
|
+
return normalisePathForIdentity(
|
|
68
|
+
path,
|
|
69
|
+
getPathNormalizationVersion(this.setting.currentSettings())
|
|
70
|
+
);
|
|
71
|
+
}
|
|
57
72
|
async getProvenance(path) {
|
|
58
73
|
if (!this.fileReflectionProvenance) return void 0;
|
|
59
74
|
try {
|
|
60
|
-
const record = await this.fileReflectionProvenance.get(path);
|
|
75
|
+
const record = await this.fileReflectionProvenance.get(this.getProvenancePath(path));
|
|
61
76
|
if (!record) return void 0;
|
|
62
77
|
const entry = await this.db.fetchEntryMeta(path, record.revision, true);
|
|
63
78
|
if (entry && !entry._deleted && !entry.deleted) {
|
|
64
79
|
return record;
|
|
65
80
|
}
|
|
66
|
-
await this.fileReflectionProvenance.delete(path);
|
|
81
|
+
if (!hasContentHash(record)) await this.fileReflectionProvenance.delete(this.getProvenancePath(path));
|
|
67
82
|
} catch (ex) {
|
|
68
83
|
this._log(`Could not read file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
|
|
69
84
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
70
85
|
}
|
|
71
86
|
return void 0;
|
|
72
87
|
}
|
|
73
|
-
async
|
|
88
|
+
async contentEvidence(content) {
|
|
89
|
+
if (!this.fileReflectionProvenance) return {};
|
|
90
|
+
try {
|
|
91
|
+
let hash = this.contentHashes.get(content);
|
|
92
|
+
if (!hash) {
|
|
93
|
+
hash = computeFileContentHash(content);
|
|
94
|
+
this.contentHashes.set(content, hash);
|
|
95
|
+
}
|
|
96
|
+
return { contentHash: await hash, contentSize: content.size };
|
|
97
|
+
} catch (ex) {
|
|
98
|
+
this.contentHashes.delete(content);
|
|
99
|
+
this._log("Could not hash file reflection content", LOG_LEVEL_VERBOSE);
|
|
100
|
+
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async setProvenance(path, revision, observedStorageMtime, content, evidence) {
|
|
74
105
|
if (!this.fileReflectionProvenance || !revision) return;
|
|
75
106
|
try {
|
|
76
|
-
await this.fileReflectionProvenance.set(path, {
|
|
107
|
+
await this.fileReflectionProvenance.set(this.getProvenancePath(path), {
|
|
77
108
|
revision,
|
|
78
|
-
observedStorageMtime
|
|
109
|
+
observedStorageMtime,
|
|
110
|
+
...evidence ?? await this.contentEvidence(content)
|
|
79
111
|
});
|
|
80
112
|
} catch (ex) {
|
|
81
113
|
this._log(`Could not record file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
|
|
@@ -85,7 +117,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
85
117
|
async deleteProvenance(path) {
|
|
86
118
|
if (!this.fileReflectionProvenance) return;
|
|
87
119
|
try {
|
|
88
|
-
await this.fileReflectionProvenance.delete(path);
|
|
120
|
+
await this.fileReflectionProvenance.delete(this.getProvenancePath(path));
|
|
89
121
|
} catch (ex) {
|
|
90
122
|
this._log(`Could not delete file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
|
|
91
123
|
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
@@ -109,7 +141,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
109
141
|
}
|
|
110
142
|
const matched = await this.findUniqueContentRevision(file);
|
|
111
143
|
if (matched) {
|
|
112
|
-
await this.setProvenance(path, matched, file.stat.mtime);
|
|
144
|
+
await this.setProvenance(path, matched, file.stat.mtime, file.body);
|
|
113
145
|
return matched;
|
|
114
146
|
}
|
|
115
147
|
return void 0;
|
|
@@ -123,12 +155,16 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
123
155
|
* identifies provenance; no match is `unknown`.
|
|
124
156
|
* `preferredPath` selects the provenance key during a rename, and
|
|
125
157
|
* `currentLoaded` supplies an already decoded candidate body when available.
|
|
126
|
-
*
|
|
127
|
-
*
|
|
158
|
+
* A valid saved content hash can prove unchanged bytes after revision
|
|
159
|
+
* pruning. It cannot reconstruct ancestry for an edited file.
|
|
128
160
|
*/
|
|
129
|
-
async classifyStorageContent(file, currentEntry, preferredPath,
|
|
161
|
+
async classifyStorageContent(file, currentEntry, preferredPath, recorded, currentLoaded) {
|
|
130
162
|
const path = preferredPath ?? file.path;
|
|
163
|
+
const recordedRevision = recorded?.revision;
|
|
131
164
|
if (recordedRevision) {
|
|
165
|
+
if (hasContentHash(recorded) && recorded.contentSize === file.body.size && (await this.contentEvidence(file.body)).contentHash === recorded.contentHash) {
|
|
166
|
+
return { kind: "unchanged", revision: recordedRevision };
|
|
167
|
+
}
|
|
132
168
|
const loadedContentIsDecoded = currentLoaded?._rev === recordedRevision;
|
|
133
169
|
const base = loadedContentIsDecoded ? currentLoaded : await this.db.fetchEntry(file, recordedRevision, true, true, true);
|
|
134
170
|
if (base && base._rev === recordedRevision && !("deleted" in base && base.deleted) && !("_deleted" in base && base._deleted)) {
|
|
@@ -137,7 +173,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
137
173
|
}
|
|
138
174
|
if (!base) {
|
|
139
175
|
const meta = await this.db.fetchEntryMeta(file, recordedRevision, true);
|
|
140
|
-
if (!meta || meta.deleted || meta._deleted) await this.deleteProvenance(path);
|
|
176
|
+
if ((!meta || meta.deleted || meta._deleted) && !hasContentHash(recorded)) await this.deleteProvenance(path);
|
|
141
177
|
}
|
|
142
178
|
}
|
|
143
179
|
const matches = currentEntry ? await this.db.findLiveContentRevisions(file, file.body) : [];
|
|
@@ -147,8 +183,8 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
147
183
|
return { kind: "unknown" };
|
|
148
184
|
}
|
|
149
185
|
/** Record provenance only when storage matches one unambiguous live leaf. */
|
|
150
|
-
async rememberMatchingLeaf(path, revision, mtime) {
|
|
151
|
-
if (revision) await this.setProvenance(path, revision, mtime);
|
|
186
|
+
async rememberMatchingLeaf(path, revision, mtime, content, evidence) {
|
|
187
|
+
if (revision) await this.setProvenance(path, revision, mtime, content, evidence);
|
|
152
188
|
}
|
|
153
189
|
/**
|
|
154
190
|
* Read the raw provenance value used by an optimistic storage snapshot.
|
|
@@ -157,7 +193,8 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
157
193
|
*/
|
|
158
194
|
async readProvenanceSnapshot(path) {
|
|
159
195
|
try {
|
|
160
|
-
|
|
196
|
+
const record = await this.fileReflectionProvenance?.get(this.getProvenancePath(path));
|
|
197
|
+
return record ? { ...record } : void 0;
|
|
161
198
|
} catch {
|
|
162
199
|
return void 0;
|
|
163
200
|
}
|
|
@@ -170,7 +207,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
170
207
|
const current = await this.storage.getStub(file.path);
|
|
171
208
|
if (!current || isFolderInfo(current) || current.path !== file.path) return false;
|
|
172
209
|
const readCurrent = await this.readFileFromStub(current);
|
|
173
|
-
return readCurrent.stat.mtime === file.stat.mtime && await isDocContentSame(readCurrent.body, file.body) && await this.readProvenanceSnapshot(file.path)
|
|
210
|
+
return readCurrent.stat.mtime === file.stat.mtime && await isDocContentSame(readCurrent.body, file.body) && sameProvenance(await this.readProvenanceSnapshot(file.path), revision);
|
|
174
211
|
}
|
|
175
212
|
/**
|
|
176
213
|
* Publish provenance after a durable database write if storage is unchanged.
|
|
@@ -178,8 +215,9 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
178
215
|
* caller to reread storage before proceeding.
|
|
179
216
|
*/
|
|
180
217
|
async finaliseStoredRevision(file, observedProvenance, storedRevision) {
|
|
218
|
+
const evidence = await this.contentEvidence(file.body);
|
|
181
219
|
if (await this.currentStorageSnapshotMatches(file, observedProvenance)) {
|
|
182
|
-
await this.setProvenance(file.path, storedRevision, file.stat.mtime);
|
|
220
|
+
await this.setProvenance(file.path, storedRevision, file.stat.mtime, file.body, evidence);
|
|
183
221
|
return true;
|
|
184
222
|
} else {
|
|
185
223
|
return false;
|
|
@@ -221,7 +259,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
221
259
|
try {
|
|
222
260
|
return await this.serializedByFileEventPaths([path], async () => {
|
|
223
261
|
const provenance = this.fileReflectionProvenance;
|
|
224
|
-
|
|
262
|
+
const provenancePath = this.getProvenancePath(path);
|
|
263
|
+
const existing = await provenance.get(provenancePath);
|
|
264
|
+
const observed = existing ? { ...existing } : void 0;
|
|
265
|
+
if (observed && (observed.revision !== expectedRevision || hasContentHash(observed))) return false;
|
|
225
266
|
const stub = await this.storage.getStub(path);
|
|
226
267
|
if (!stub || isFolderInfo(stub) || stub.path !== path) return false;
|
|
227
268
|
const file = await this.readFileFromStub(stub);
|
|
@@ -231,12 +272,14 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
231
272
|
const entry = await this.db.fetchEntry(path, expectedRevision, true, true, true);
|
|
232
273
|
if (!entry || entry._rev !== expectedRevision || entry.deleted || entry._deleted) return false;
|
|
233
274
|
if (!await isDocContentSame(readContent(entry), file.body)) return false;
|
|
275
|
+
const evidence = await this.contentEvidence(file.body);
|
|
276
|
+
if (!evidence.contentHash) return false;
|
|
234
277
|
const latest = await this.db.fetchEntryMeta(path, void 0, true);
|
|
235
278
|
if (!latest || latest._rev !== expectedRevision || latest.deleted || latest._deleted) return false;
|
|
236
279
|
if ((await this.db.getConflictedRevs(path)).length > 0) return false;
|
|
237
|
-
if (!await this.currentStorageSnapshotMatches(file,
|
|
238
|
-
if (await provenance.get(
|
|
239
|
-
await provenance.set(
|
|
280
|
+
if (!await this.currentStorageSnapshotMatches(file, observed)) return false;
|
|
281
|
+
if (!sameProvenance(await provenance.get(provenancePath), observed)) return false;
|
|
282
|
+
await provenance.set(provenancePath, { revision: expectedRevision, observedStorageMtime: file.stat.mtime, ...evidence });
|
|
240
283
|
return true;
|
|
241
284
|
});
|
|
242
285
|
} catch (ex) {
|
|
@@ -300,7 +343,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
300
343
|
if (!baseEntry.deleted && !baseEntry._deleted) {
|
|
301
344
|
const loadedBase = await this.db.fetchEntry(file, baseRevision, true, true);
|
|
302
345
|
if (loadedBase && await isDocContentSame(getDocDataAsArray(loadedBase.data), readFile.body)) {
|
|
303
|
-
await this.setProvenance(file.path, baseRevision, readFile.stat.mtime);
|
|
346
|
+
await this.setProvenance(file.path, baseRevision, readFile.stat.mtime, readFile.body);
|
|
304
347
|
await this.queueConflictCheckFor(file.path);
|
|
305
348
|
return true;
|
|
306
349
|
}
|
|
@@ -316,7 +359,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
316
359
|
if (storedRevision === false) {
|
|
317
360
|
return false;
|
|
318
361
|
}
|
|
319
|
-
await this.setProvenance(file.path, storedRevision, readFile.stat.mtime);
|
|
362
|
+
await this.setProvenance(file.path, storedRevision, readFile.stat.mtime, readFile.body);
|
|
320
363
|
await this.queueConflictCheckFor(file.path);
|
|
321
364
|
return true;
|
|
322
365
|
}
|
|
@@ -359,7 +402,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
359
402
|
observedProvenance,
|
|
360
403
|
entry ? { _rev: entry._rev, data: readContent(entry) } : void 0
|
|
361
404
|
);
|
|
362
|
-
|
|
405
|
+
const upgradeCurrentRecord = classification.kind === "unchanged" && !isConflicted && classification.revision === currentEntry._rev && !hasContentHash(observedProvenance);
|
|
406
|
+
const evidence = upgradeCurrentRecord || classification.kind === "matching-leaf" && classification.revision ? await this.contentEvidence(readFile.body) : void 0;
|
|
407
|
+
const snapshotMatches = retryIfStorageChanges || upgradeCurrentRecord ? await this.currentStorageSnapshotMatches(readFile, observedProvenance) : true;
|
|
408
|
+
if (retryIfStorageChanges && !snapshotMatches) {
|
|
363
409
|
return await this.retryCurrentStorage(file.path, preferredBasePath, snapshotRetry);
|
|
364
410
|
}
|
|
365
411
|
if (classification.kind === "unchanged") {
|
|
@@ -369,12 +415,15 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
369
415
|
if (!entry) return false;
|
|
370
416
|
return await this.dbToStorageCore(file.path, file);
|
|
371
417
|
} else {
|
|
418
|
+
if (upgradeCurrentRecord && snapshotMatches) {
|
|
419
|
+
await this.setProvenance(file.path, classification.revision, readFile.stat.mtime, readFile.body, evidence);
|
|
420
|
+
}
|
|
372
421
|
this.path.markChangesAreSame(readFile, readFile.stat.mtime, currentEntry.mtime);
|
|
373
422
|
}
|
|
374
423
|
return true;
|
|
375
424
|
}
|
|
376
425
|
if (classification.kind === "matching-leaf") {
|
|
377
|
-
await this.rememberMatchingLeaf(file.path, classification.revision, readFile.stat.mtime);
|
|
426
|
+
await this.rememberMatchingLeaf(file.path, classification.revision, readFile.stat.mtime, readFile.body, evidence);
|
|
378
427
|
if (isConflicted) await this.queueConflictCheckFor(file.path);
|
|
379
428
|
return true;
|
|
380
429
|
}
|
|
@@ -384,7 +433,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
384
433
|
if (retryIfStorageChanges && !await this.finaliseStoredRevision(readFile, observedProvenance, storedRevision2)) {
|
|
385
434
|
return await this.retryCurrentStorage(file.path, preferredBasePath, snapshotRetry);
|
|
386
435
|
}
|
|
387
|
-
if (!retryIfStorageChanges) await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
|
|
436
|
+
if (!retryIfStorageChanges) await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime, readFile.body);
|
|
388
437
|
if (isConflicted || classification.kind === "unknown") await this.queueConflictCheckFor(file.path);
|
|
389
438
|
return true;
|
|
390
439
|
}
|
|
@@ -393,7 +442,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
393
442
|
if (baseRevision) {
|
|
394
443
|
const baseEntry = await this.db.fetchEntry(file, baseRevision, true, true);
|
|
395
444
|
if (baseEntry && await isDocContentSame(getDocDataAsArray(baseEntry.data), readFile.body) && !force) {
|
|
396
|
-
await this.setProvenance(file.path, baseRevision, readFile.stat.mtime);
|
|
445
|
+
await this.setProvenance(file.path, baseRevision, readFile.stat.mtime, readFile.body);
|
|
397
446
|
this._log(`File ${file.path} is not changed on its displayed conflict branch`, LOG_LEVEL_VERBOSE);
|
|
398
447
|
return true;
|
|
399
448
|
}
|
|
@@ -402,7 +451,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
402
451
|
if (preferredBasePath && preferredBasePath !== file.path) {
|
|
403
452
|
await this.deleteProvenance(preferredBasePath);
|
|
404
453
|
}
|
|
405
|
-
await this.setProvenance(file.path, storedRevision3, readFile.stat.mtime);
|
|
454
|
+
await this.setProvenance(file.path, storedRevision3, readFile.stat.mtime, readFile.body);
|
|
406
455
|
await this.queueConflictCheckFor(file.path);
|
|
407
456
|
return true;
|
|
408
457
|
}
|
|
@@ -425,7 +474,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
425
474
|
if (preferredBasePath && preferredBasePath !== file.path) {
|
|
426
475
|
await this.deleteProvenance(preferredBasePath);
|
|
427
476
|
}
|
|
428
|
-
await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
|
|
477
|
+
await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime, readFile.body);
|
|
429
478
|
await this.queueConflictCheckFor(file.path);
|
|
430
479
|
return true;
|
|
431
480
|
}
|
|
@@ -435,7 +484,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
435
484
|
if (preferredBasePath && preferredBasePath !== file.path) {
|
|
436
485
|
await this.deleteProvenance(preferredBasePath);
|
|
437
486
|
}
|
|
438
|
-
await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
|
|
487
|
+
await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime, readFile.body);
|
|
439
488
|
return true;
|
|
440
489
|
}
|
|
441
490
|
let shouldApplied = false;
|
|
@@ -451,7 +500,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
451
500
|
}
|
|
452
501
|
}
|
|
453
502
|
if (!shouldApplied) {
|
|
454
|
-
await this.setProvenance(file.path, entry._rev, readFile.stat.mtime);
|
|
503
|
+
await this.setProvenance(file.path, entry._rev, readFile.stat.mtime, readFile.body);
|
|
455
504
|
this._log(`File ${file.path} is not changed`, LOG_LEVEL_VERBOSE);
|
|
456
505
|
return true;
|
|
457
506
|
}
|
|
@@ -461,7 +510,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
461
510
|
if (preferredBasePath && preferredBasePath !== file.path) {
|
|
462
511
|
await this.deleteProvenance(preferredBasePath);
|
|
463
512
|
}
|
|
464
|
-
await this.setProvenance(file.path, storedRevision, readFile.stat.mtime);
|
|
513
|
+
await this.setProvenance(file.path, storedRevision, readFile.stat.mtime, readFile.body);
|
|
465
514
|
return true;
|
|
466
515
|
}
|
|
467
516
|
async deleteFileFromDB(info) {
|
|
@@ -813,20 +862,30 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
813
862
|
);
|
|
814
863
|
return false;
|
|
815
864
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
);
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
865
|
+
const pathNormalizationVersion = getPathNormalizationVersion(settings);
|
|
866
|
+
const sameNormalisedPath = pathNormalizationVersion === 1 && normalisePathForIdentity(existDoc.path, pathNormalizationVersion) === normalisePathForIdentity(path, pathNormalizationVersion);
|
|
867
|
+
if (!sameNormalisedPath) {
|
|
868
|
+
const existingParent = getParentPath(existDoc.path);
|
|
869
|
+
const targetParent = getParentPath(path);
|
|
870
|
+
const sameNormalisedParent = pathNormalizationVersion === 1 && normalisePathForIdentity(existingParent, pathNormalizationVersion) === normalisePathForIdentity(targetParent, pathNormalizationVersion);
|
|
871
|
+
if (existingParent !== targetParent && !sameNormalisedParent) {
|
|
872
|
+
this._log(
|
|
873
|
+
`Refusing to apply a filename case change across differently cased parent directories: ${existDoc.path} -> ${path}`,
|
|
874
|
+
LOG_LEVEL_NOTICE
|
|
875
|
+
);
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
const renamedFile = await this.storage.renameFile(existDoc, path);
|
|
879
|
+
if (!renamedFile) {
|
|
880
|
+
this._log(
|
|
881
|
+
`Could not apply the stored filename case: ${existDoc.path} -> ${path}`,
|
|
882
|
+
LOG_LEVEL_NOTICE
|
|
883
|
+
);
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
await this.deleteProvenance(existDoc.path);
|
|
887
|
+
existDoc = renamedFile;
|
|
827
888
|
}
|
|
828
|
-
await this.deleteProvenance(existDoc.path);
|
|
829
|
-
existDoc = renamedFile;
|
|
830
889
|
}
|
|
831
890
|
const docRead = await this.db.fetchEntryFromMeta(docEntry);
|
|
832
891
|
if (!docRead) {
|
|
@@ -843,10 +902,12 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
843
902
|
}
|
|
844
903
|
}
|
|
845
904
|
const docData = readContent(docRead);
|
|
905
|
+
const reflectedContent = readAsBlob(docRead);
|
|
906
|
+
const reflectedEvidence = await this.contentEvidence(reflectedContent);
|
|
846
907
|
if (allowExistingConflicts && existDoc && !force) {
|
|
847
908
|
const readFile = await this.readFileFromStub(existDoc);
|
|
848
909
|
if (await isDocContentSame(docData, readFile.body)) {
|
|
849
|
-
await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime);
|
|
910
|
+
await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime, reflectedContent, reflectedEvidence);
|
|
850
911
|
this.path.markChangesAreSame(docRead, docRead.mtime, existDoc.stat.mtime);
|
|
851
912
|
return true;
|
|
852
913
|
}
|
|
@@ -868,7 +929,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
868
929
|
}
|
|
869
930
|
}
|
|
870
931
|
if (!shouldApplied) {
|
|
871
|
-
await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime);
|
|
932
|
+
await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime, reflectedContent, reflectedEvidence);
|
|
872
933
|
this._log(`File ${docRead.path} is not changed`, LOG_LEVEL_VERBOSE);
|
|
873
934
|
return true;
|
|
874
935
|
}
|
|
@@ -913,10 +974,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
913
974
|
this.storage.triggerFileEvent(mode, path);
|
|
914
975
|
if (ret && this.fileReflectionProvenance) {
|
|
915
976
|
const storedStat = await this.storage.stat(path);
|
|
916
|
-
if (safeSnapshot && await this.readProvenanceSnapshot(path)
|
|
977
|
+
if (safeSnapshot && !sameProvenance(await this.readProvenanceSnapshot(path), safeSnapshot.revision)) {
|
|
917
978
|
return ret;
|
|
918
979
|
}
|
|
919
|
-
await this.setProvenance(path, docEntry._rev, storedStat?.mtime);
|
|
980
|
+
await this.setProvenance(path, docEntry._rev, storedStat?.mtime, reflectedContent, reflectedEvidence);
|
|
920
981
|
}
|
|
921
982
|
return ret;
|
|
922
983
|
}
|
|
@@ -954,7 +1015,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
954
1015
|
return { preserved: false, snapshot };
|
|
955
1016
|
}
|
|
956
1017
|
if (classification.kind === "matching-leaf") {
|
|
957
|
-
await this.rememberMatchingLeaf(path, classification.revision, readFile.stat.mtime);
|
|
1018
|
+
await this.rememberMatchingLeaf(path, classification.revision, readFile.stat.mtime, readFile.body);
|
|
958
1019
|
await this.queueConflictCheckFor(path);
|
|
959
1020
|
return { preserved: true };
|
|
960
1021
|
}
|
|
@@ -60,13 +60,14 @@ export interface IAPIService {
|
|
|
60
60
|
export interface IPathService {
|
|
61
61
|
id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix;
|
|
62
62
|
path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID>;
|
|
63
|
-
path2idWithSettings(filename: FilePathWithPrefix | FilePath, setting:
|
|
63
|
+
path2idWithSettings(filename: FilePathWithPrefix | FilePath, setting: PathIDSettings, prefix?: string): Promise<DocumentID>;
|
|
64
64
|
getPath(entry: AnyEntry): FilePathWithPrefix;
|
|
65
65
|
markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined;
|
|
66
66
|
unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void;
|
|
67
67
|
compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN;
|
|
68
68
|
isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN;
|
|
69
69
|
}
|
|
70
|
+
export type PathIDSettings = Pick<RemoteDBSettings, "encrypt" | "usePathObfuscation" | "passphrase" | "handleFilenameCaseSensitive" | "idDerivationVersion" | "idDerivationKey"> & Partial<Pick<RemoteDBSettings, "pathNormalizationVersion">>;
|
|
70
71
|
export interface openDatabaseParameters {
|
|
71
72
|
replicator: ReplicatorService;
|
|
72
73
|
databaseEvents: DatabaseEventService;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { DocumentID, EntryHasPath, FilePathWithPrefix, FilePath, AnyEntry,
|
|
2
|
-
import type { IPathService, ISettingService } from "./IService.js";
|
|
1
|
+
import type { DocumentID, EntryHasPath, FilePathWithPrefix, FilePath, AnyEntry, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
|
|
2
|
+
import type { IPathService, ISettingService, PathIDSettings } from "./IService.js";
|
|
3
3
|
import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
|
|
4
4
|
import type { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../../common/models/shared.const.symbols.js";
|
|
5
5
|
export interface PathServiceDependencies {
|
|
@@ -37,7 +37,7 @@ export declare abstract class PathService<T extends ServiceContext = ServiceCont
|
|
|
37
37
|
*/
|
|
38
38
|
path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID>;
|
|
39
39
|
/** Convert a path using the caller's settings snapshot and this host's path normalisation. */
|
|
40
|
-
path2idWithSettings(filename: FilePathWithPrefix | FilePath, setting:
|
|
40
|
+
path2idWithSettings(filename: FilePathWithPrefix | FilePath, setting: PathIDSettings, prefix?: string): Promise<DocumentID>;
|
|
41
41
|
getPath(entry: AnyEntry): FilePathWithPrefix;
|
|
42
42
|
abstract markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined;
|
|
43
43
|
abstract unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getPathNormalizationVersion, normalisePathForIdentity } from "../../common/pathNormalization.js";
|
|
1
2
|
import { ServiceBase } from "./ServiceBase.js";
|
|
2
3
|
import { addPrefix, expandFilePathPrefix, id2path_base, path2id_base } from "../../string_and_binary/path.js";
|
|
3
4
|
import { configuredIdKey } from "../../common/idDerivation.js";
|
|
@@ -18,9 +19,9 @@ class PathService extends ServiceBase {
|
|
|
18
19
|
const [prefix, path] = expandFilePathPrefix(filename);
|
|
19
20
|
return prefix + this.normalizePath(path);
|
|
20
21
|
}
|
|
21
|
-
async _path2id(filename, obfuscatePassphrase, caseInsensitive, idDerivationKey) {
|
|
22
|
+
async _path2id(filename, obfuscatePassphrase, caseInsensitive, idDerivationKey, pathNormalizationVersion) {
|
|
22
23
|
const [prefix, path] = expandFilePathPrefix(filename);
|
|
23
|
-
const fixedPath = prefix + this.normalizePath(path);
|
|
24
|
+
const fixedPath = prefix + normalisePathForIdentity(this.normalizePath(path), pathNormalizationVersion);
|
|
24
25
|
const out = await path2id_base(fixedPath, obfuscatePassphrase, caseInsensitive, idDerivationKey);
|
|
25
26
|
return out;
|
|
26
27
|
}
|
|
@@ -49,13 +50,15 @@ class PathService extends ServiceBase {
|
|
|
49
50
|
}
|
|
50
51
|
/** Convert a path using the caller's settings snapshot and this host's path normalisation. */
|
|
51
52
|
async path2idWithSettings(filename, setting, prefix) {
|
|
53
|
+
const pathNormalizationVersion = getPathNormalizationVersion(setting);
|
|
52
54
|
const destPath = addPrefix(filename, prefix ?? "");
|
|
53
55
|
const pathObfuscationPassphrase = this.getPathObfuscationPassphrase?.() ?? (setting.usePathObfuscation ? setting.passphrase : false);
|
|
54
56
|
return await this._path2id(
|
|
55
57
|
destPath,
|
|
56
58
|
pathObfuscationPassphrase,
|
|
57
59
|
!setting.handleFilenameCaseSensitive,
|
|
58
|
-
setting.encrypt && configuredIdKey(setting) || void 0
|
|
60
|
+
setting.encrypt && configuredIdKey(setting) || void 0,
|
|
61
|
+
pathNormalizationVersion
|
|
59
62
|
);
|
|
60
63
|
}
|
|
61
64
|
getPath(entry) {
|
package/dist/settings.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { DEFAULT_SETTINGS, NEW_VAULT_SETTINGS, P2P_DEFAULT_SETTINGS, SETTINGS_SC
|
|
|
7
7
|
export { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, } from "./common/models/setting.const.preferred.js";
|
|
8
8
|
export { CURRENT_SETTING_VERSION } from "./common/models/setting.const.js";
|
|
9
9
|
export { deriveIdKey, deriveOrImportIdKey, formatIdRecoveryCode, configuredIdKey, ID_DERIVATION_VERSION, ID_RECOVERY_CODE_PREFIX, } from "./common/idDerivation.js";
|
|
10
|
+
export { getPathNormalizationVersion, normalisePathForIdentity } from "./common/pathNormalization.js";
|
|
11
|
+
export type { PathNormalizationVersion, PathIdentitySettings } from "./common/models/setting.type.js";
|
|
10
12
|
export { prepareSettingsForLoad, SettingsMigrationReviewCodes, type PreparedSettings, type SettingsMigrationReviewCode, type SettingsMigrationReviewReason, type SettingsMigrationState, } from "./common/models/setting.lifecycle.js";
|
|
11
13
|
export { assessTweakCompatibility } from "./common/models/tweak.compatibility.js";
|
|
12
14
|
export type { TweakAssessment, TweakAssessmentEntry, TweakAssessmentRelation, TweakAssessmentValue, TweakTransition, TweakTransitionReason, } from "./common/models/tweak.compatibility.js";
|
package/dist/settings.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
ID_DERIVATION_VERSION,
|
|
21
21
|
ID_RECOVERY_CODE_PREFIX
|
|
22
22
|
} from "./common/idDerivation.js";
|
|
23
|
+
import { getPathNormalizationVersion, normalisePathForIdentity } from "./common/pathNormalization.js";
|
|
23
24
|
import {
|
|
24
25
|
prepareSettingsForLoad,
|
|
25
26
|
SettingsMigrationReviewCodes
|
|
@@ -53,6 +54,8 @@ export {
|
|
|
53
54
|
deriveIdKey,
|
|
54
55
|
deriveOrImportIdKey,
|
|
55
56
|
formatIdRecoveryCode,
|
|
57
|
+
getPathNormalizationVersion,
|
|
56
58
|
mergeMarkdownSettings,
|
|
59
|
+
normalisePathForIdentity,
|
|
57
60
|
prepareSettingsForLoad
|
|
58
61
|
};
|