@vrtmrz/livesync-commonlib 0.1.0-rc.8 → 0.1.0
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 +5 -2
- package/dist/common/models/setting.lifecycle.d.ts +1 -0
- package/dist/common/models/setting.lifecycle.js +11 -7
- package/dist/interfaces/DatabaseFileAccess.d.ts +8 -0
- package/dist/interfaces/FileHandler.d.ts +17 -1
- package/dist/interfaces/FileReflectionProvenance.d.ts +32 -0
- package/dist/interfaces/FileReflectionProvenance.js +25 -0
- package/dist/managers/ConflictManager.js +42 -9
- package/dist/managers/EntryManager/EntryManager.d.ts +1 -0
- package/dist/managers/EntryManager/EntryManager.js +5 -1
- package/dist/managers/EntryManager/EntryManagerImpls.d.ts +1 -0
- package/dist/managers/EntryManager/EntryManagerImpls.js +29 -1
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +1 -0
- package/dist/pouchdb/LiveSyncLocalDB.js +3 -0
- package/dist/serviceFeatures/offlineScanner.js +3 -2
- package/dist/serviceFeatures/remoteConfig.d.ts +2 -2
- package/dist/serviceFeatures/remoteConfig.js +2 -2
- package/dist/serviceModules/ServiceDatabaseFileAccessBase.d.ts +5 -0
- package/dist/serviceModules/ServiceDatabaseFileAccessBase.js +69 -13
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +22 -1
- package/dist/serviceModules/ServiceFileHandlerBase.js +323 -48
- package/dist/services/base/KeyValueDBService.d.ts +1 -1
- package/dist/services/base/KeyValueDBService.js +3 -1
- package/dist/worker/bgWorker.js +1 -1
- package/docs/conflict-resolution.md +173 -0
- package/docs/development.md +6 -0
- package/docs/proven-in-use.md +4 -0
- package/docs/releasing.md +10 -7
- package/docs/settings-lifecycle.md +25 -2
- package/package.json +6 -1
|
@@ -45,6 +45,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
45
45
|
path;
|
|
46
46
|
setting;
|
|
47
47
|
vault;
|
|
48
|
+
fileReflectionProvenance;
|
|
48
49
|
constructor(services) {
|
|
49
50
|
super(services);
|
|
50
51
|
this.events = services.events;
|
|
@@ -54,6 +55,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
54
55
|
this.path = services.path;
|
|
55
56
|
this.setting = services.setting;
|
|
56
57
|
this.vault = services.vault;
|
|
58
|
+
this.fileReflectionProvenance = services.fileReflectionProvenance;
|
|
57
59
|
services.fileProcessing.processFileEvent.addHandler(this._anyHandlerProcessesFileEvent.bind(this), 100);
|
|
58
60
|
services.replication.processSynchroniseResult.addHandler(this._anyProcessReplicatedDoc.bind(this), 100);
|
|
59
61
|
}
|
|
@@ -63,6 +65,66 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
63
65
|
get storage() {
|
|
64
66
|
return this.storageAccess;
|
|
65
67
|
}
|
|
68
|
+
async getProvenance(path) {
|
|
69
|
+
if (!this.fileReflectionProvenance) return void 0;
|
|
70
|
+
try {
|
|
71
|
+
const record = await this.fileReflectionProvenance.get(path);
|
|
72
|
+
if (!record) return void 0;
|
|
73
|
+
const entry = await this.db.fetchEntryMeta(path, record.revision, true);
|
|
74
|
+
if (entry && !entry._deleted && !entry.deleted) {
|
|
75
|
+
return record;
|
|
76
|
+
}
|
|
77
|
+
await this.fileReflectionProvenance.delete(path);
|
|
78
|
+
} catch (ex) {
|
|
79
|
+
this._log(`Could not read file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
|
|
80
|
+
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
81
|
+
}
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
async setProvenance(path, revision, observedStorageMtime) {
|
|
85
|
+
if (!this.fileReflectionProvenance || !revision) return;
|
|
86
|
+
try {
|
|
87
|
+
await this.fileReflectionProvenance.set(path, {
|
|
88
|
+
revision,
|
|
89
|
+
observedStorageMtime
|
|
90
|
+
});
|
|
91
|
+
} catch (ex) {
|
|
92
|
+
this._log(`Could not record file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
|
|
93
|
+
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async deleteProvenance(path) {
|
|
97
|
+
if (!this.fileReflectionProvenance) return;
|
|
98
|
+
try {
|
|
99
|
+
await this.fileReflectionProvenance.delete(path);
|
|
100
|
+
} catch (ex) {
|
|
101
|
+
this._log(`Could not delete file reflection provenance for ${path}`, LOG_LEVEL_VERBOSE);
|
|
102
|
+
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async findUniqueContentRevision(file) {
|
|
106
|
+
try {
|
|
107
|
+
const revisions = await this.db.findContentRevisions(file, file.body);
|
|
108
|
+
return revisions.length === 1 ? revisions[0] : void 0;
|
|
109
|
+
} catch (ex) {
|
|
110
|
+
this._log(`Could not reconstruct file reflection provenance for ${file.path}`, LOG_LEVEL_VERBOSE);
|
|
111
|
+
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async getProvenBaseRevision(file, preferredPath) {
|
|
116
|
+
const path = preferredPath ?? file.path;
|
|
117
|
+
const recorded = await this.getProvenance(path) ?? (preferredPath && preferredPath !== file.path ? await this.getProvenance(file.path) : void 0);
|
|
118
|
+
if (recorded) {
|
|
119
|
+
return recorded.revision;
|
|
120
|
+
}
|
|
121
|
+
const matched = await this.findUniqueContentRevision(file);
|
|
122
|
+
if (matched) {
|
|
123
|
+
await this.setProvenance(path, matched, file.stat.mtime);
|
|
124
|
+
return matched;
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
66
128
|
getPath(entry) {
|
|
67
129
|
return this.path.getPath(entry);
|
|
68
130
|
}
|
|
@@ -85,6 +147,62 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
85
147
|
return file;
|
|
86
148
|
}
|
|
87
149
|
async storeFileToDB(info, force = false, onlyChunks = false) {
|
|
150
|
+
return await this.storeFileToDBFromRevision(info, force, onlyChunks);
|
|
151
|
+
}
|
|
152
|
+
async storeFileToDBWithBaseRevision(info, baseRevision, createIfDifferent = true) {
|
|
153
|
+
const file = await this.infoToStub(info);
|
|
154
|
+
if (file == null) {
|
|
155
|
+
this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (file.isInternal) {
|
|
159
|
+
this._log(
|
|
160
|
+
`Internal file ${file.path} is not allowed to be stored through the ordinary file handler`,
|
|
161
|
+
LOG_LEVEL_VERBOSE
|
|
162
|
+
);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const [baseEntry, currentEntry, conflictedRevisions] = await Promise.all([
|
|
166
|
+
this.db.fetchEntryMeta(file, baseRevision, true),
|
|
167
|
+
this.db.fetchEntryMeta(file, void 0, true),
|
|
168
|
+
this.db.getConflictedRevs(file)
|
|
169
|
+
]);
|
|
170
|
+
const liveRevisions = /* @__PURE__ */ new Set([
|
|
171
|
+
...currentEntry && currentEntry._rev ? [currentEntry._rev] : [],
|
|
172
|
+
...conflictedRevisions
|
|
173
|
+
]);
|
|
174
|
+
if (!baseEntry || baseEntry._rev !== baseRevision || !liveRevisions.has(baseRevision)) {
|
|
175
|
+
this._log(
|
|
176
|
+
`Could not store ${file.path} on revision ${baseRevision}; the selected revision is no longer live`,
|
|
177
|
+
LOG_LEVEL_NOTICE
|
|
178
|
+
);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const readFile = await this.readFileFromStub(file);
|
|
182
|
+
if (!baseEntry.deleted && !baseEntry._deleted) {
|
|
183
|
+
const loadedBase = await this.db.fetchEntry(file, baseRevision, true, true);
|
|
184
|
+
if (loadedBase && await isDocContentSame(getDocDataAsArray(loadedBase.data), readFile.body)) {
|
|
185
|
+
await this.setProvenance(file.path, baseRevision, readFile.stat.mtime);
|
|
186
|
+
await this.conflict.queueCheckFor(file.path);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!createIfDifferent) {
|
|
191
|
+
this._log(
|
|
192
|
+
`Could not mark ${file.path} as revision ${baseRevision}; the storage content differs`,
|
|
193
|
+
LOG_LEVEL_NOTICE
|
|
194
|
+
);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const storedRevision = await this.db.storeWithBaseRevision(readFile, baseRevision, true);
|
|
198
|
+
if (storedRevision === false) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
await this.setProvenance(file.path, storedRevision, readFile.stat.mtime);
|
|
202
|
+
await this.conflict.queueCheckFor(file.path);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
async storeFileToDBFromRevision(info, force = false, onlyChunks = false, preferredBasePath) {
|
|
88
206
|
const file = await this.infoToStub(info);
|
|
89
207
|
if (file == null) {
|
|
90
208
|
this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
|
@@ -97,27 +215,70 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
97
215
|
);
|
|
98
216
|
return false;
|
|
99
217
|
}
|
|
218
|
+
if (onlyChunks) {
|
|
219
|
+
const readFile2 = await this.readFileFromStub(file);
|
|
220
|
+
return await this.db.createChunks(readFile2, force, true);
|
|
221
|
+
}
|
|
222
|
+
const readFile = await this.readFileFromStub(file);
|
|
100
223
|
const entry = await this.db.fetchEntry(file, void 0, true, true);
|
|
224
|
+
const conflictedRevs = await this.db.getConflictedRevs(file);
|
|
225
|
+
const isConflicted = conflictedRevs.length > 0;
|
|
226
|
+
if (isConflicted) {
|
|
227
|
+
const baseRevision = await this.getProvenBaseRevision(readFile, preferredBasePath);
|
|
228
|
+
if (baseRevision) {
|
|
229
|
+
const baseEntry = await this.db.fetchEntry(file, baseRevision, true, true);
|
|
230
|
+
if (baseEntry && await isDocContentSame(getDocDataAsArray(baseEntry.data), readFile.body) && !force) {
|
|
231
|
+
await this.setProvenance(file.path, baseRevision, readFile.stat.mtime);
|
|
232
|
+
this._log(`File ${file.path} is not changed on its displayed conflict branch`, LOG_LEVEL_VERBOSE);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
const storedRevision3 = await this.db.storeWithBaseRevision(readFile, baseRevision, true);
|
|
236
|
+
if (storedRevision3 === false) return false;
|
|
237
|
+
if (preferredBasePath && preferredBasePath !== file.path) {
|
|
238
|
+
await this.deleteProvenance(preferredBasePath);
|
|
239
|
+
}
|
|
240
|
+
await this.setProvenance(file.path, storedRevision3, readFile.stat.mtime);
|
|
241
|
+
await this.conflict.queueCheckFor(file.path);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
const currentEntry = entry || await this.db.fetchEntryMeta(file, void 0, true);
|
|
245
|
+
const currentRevision = currentEntry && currentEntry._rev;
|
|
246
|
+
if (!currentRevision) {
|
|
247
|
+
this._log(
|
|
248
|
+
`Could not preserve the unknown conflict branch for ${file.path}; no current revision is available`,
|
|
249
|
+
LOG_LEVEL_NOTICE
|
|
250
|
+
);
|
|
251
|
+
await this.conflict.queueCheckFor(file.path);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const storedRevision2 = await this.db.storeAsConflictedRevisionWithResult(readFile, currentRevision, true);
|
|
255
|
+
if (storedRevision2 === false) {
|
|
256
|
+
this._log(`Could not preserve the unknown conflict branch for ${file.path}`, LOG_LEVEL_NOTICE);
|
|
257
|
+
await this.conflict.queueCheckFor(file.path);
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (preferredBasePath && preferredBasePath !== file.path) {
|
|
261
|
+
await this.deleteProvenance(preferredBasePath);
|
|
262
|
+
}
|
|
263
|
+
await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
|
|
264
|
+
await this.conflict.queueCheckFor(file.path);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
101
267
|
if (!entry || entry.deleted || entry._deleted) {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return await this.db.createChunks(readFile, false, true);
|
|
268
|
+
const storedRevision2 = await this.db.storeWithBaseRevision(readFile, entry && entry._rev, true);
|
|
269
|
+
if (storedRevision2 === false) return false;
|
|
270
|
+
if (preferredBasePath && preferredBasePath !== file.path) {
|
|
271
|
+
await this.deleteProvenance(preferredBasePath);
|
|
107
272
|
}
|
|
273
|
+
await this.setProvenance(file.path, storedRevision2, readFile.stat.mtime);
|
|
274
|
+
return true;
|
|
108
275
|
}
|
|
109
276
|
let shouldApplied = false;
|
|
110
277
|
if (!force && !onlyChunks) {
|
|
111
278
|
if (this.path.compareFileFreshness(file, entry) !== EVEN) {
|
|
112
279
|
shouldApplied = true;
|
|
113
280
|
}
|
|
114
|
-
let readFile = void 0;
|
|
115
281
|
if (!shouldApplied) {
|
|
116
|
-
readFile = await this.readFileFromStub(file);
|
|
117
|
-
if (!readFile) {
|
|
118
|
-
this._log(`File ${file.path} is not exist on the storage`, LOG_LEVEL_NOTICE);
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
282
|
if (await isDocContentSame(getDocDataAsArray(entry.data), readFile.body)) {
|
|
122
283
|
this.path.markChangesAreSame(readFile, readFile.stat.mtime, entry.mtime);
|
|
123
284
|
} else {
|
|
@@ -125,28 +286,23 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
125
286
|
}
|
|
126
287
|
}
|
|
127
288
|
if (!shouldApplied) {
|
|
289
|
+
await this.setProvenance(file.path, entry._rev, readFile.stat.mtime);
|
|
128
290
|
this._log(`File ${file.path} is not changed`, LOG_LEVEL_VERBOSE);
|
|
129
291
|
return true;
|
|
130
292
|
}
|
|
131
|
-
if (!readFile) readFile = await this.readFileFromStub(file);
|
|
132
|
-
if (onlyChunks) {
|
|
133
|
-
return await this.db.createChunks(readFile, false, true);
|
|
134
|
-
} else {
|
|
135
|
-
return await this.db.store(readFile, false, true);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
const readFile = await this.readFileFromStub(file);
|
|
139
|
-
if (onlyChunks) {
|
|
140
|
-
return await this.db.createChunks(readFile, true, true);
|
|
141
|
-
} else {
|
|
142
|
-
return await this.db.store(readFile, true, true);
|
|
143
|
-
}
|
|
144
293
|
}
|
|
294
|
+
const storedRevision = await this.db.storeWithBaseRevision(readFile, entry._rev, true);
|
|
295
|
+
if (storedRevision === false) return false;
|
|
296
|
+
if (preferredBasePath && preferredBasePath !== file.path) {
|
|
297
|
+
await this.deleteProvenance(preferredBasePath);
|
|
298
|
+
}
|
|
299
|
+
await this.setProvenance(file.path, storedRevision, readFile.stat.mtime);
|
|
300
|
+
return true;
|
|
145
301
|
}
|
|
146
302
|
async deleteFileFromDB(info) {
|
|
147
303
|
const file = await this.infoToStub(info);
|
|
304
|
+
const path = typeof info === "string" ? info : tryGetFilePath(info);
|
|
148
305
|
if (file == null) {
|
|
149
|
-
const path = typeof info === "string" ? info : tryGetFilePath(info);
|
|
150
306
|
if (path === void 0) {
|
|
151
307
|
this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
|
152
308
|
return false;
|
|
@@ -159,8 +315,27 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
159
315
|
);
|
|
160
316
|
return false;
|
|
161
317
|
}
|
|
318
|
+
const conflictedRevs2 = await this.db.getConflictedRevs(path);
|
|
319
|
+
if (conflictedRevs2.length > 0) {
|
|
320
|
+
const provenance = await this.getProvenance(path);
|
|
321
|
+
if (!provenance) {
|
|
322
|
+
this._log(
|
|
323
|
+
`The deleted storage file ${path} has conflicts, but its displayed revision is unknown; preserving every database branch`,
|
|
324
|
+
LOG_LEVEL_NOTICE
|
|
325
|
+
);
|
|
326
|
+
await this.conflict.queueCheckFor(path);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
const storedRevision = await this.db.storeDeletionWithBaseRevision(path, provenance.revision);
|
|
330
|
+
if (storedRevision === false) return false;
|
|
331
|
+
await this.deleteProvenance(path);
|
|
332
|
+
await this.conflict.queueCheckFor(path);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
162
335
|
this._log(`File ${path} is missing on storage; deleting from the database by path`, LOG_LEVEL_INFO);
|
|
163
|
-
|
|
336
|
+
const deleted2 = await this.db.delete(path);
|
|
337
|
+
if (deleted2) await this.deleteProvenance(path);
|
|
338
|
+
return deleted2;
|
|
164
339
|
}
|
|
165
340
|
if (file.isInternal) {
|
|
166
341
|
this._log(
|
|
@@ -176,9 +351,31 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
176
351
|
}
|
|
177
352
|
const conflictedRevs = await this.db.getConflictedRevs(file);
|
|
178
353
|
if (conflictedRevs.length > 0) {
|
|
179
|
-
|
|
354
|
+
let baseRevision = (await this.getProvenance(file.path))?.revision;
|
|
355
|
+
if (!baseRevision) {
|
|
356
|
+
try {
|
|
357
|
+
const readFile = await this.readFileFromStub(file);
|
|
358
|
+
baseRevision = await this.findUniqueContentRevision(readFile);
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!baseRevision) {
|
|
363
|
+
this._log(
|
|
364
|
+
`The deleted storage file ${file.path} has conflicts, but its displayed revision is unknown; preserving every database branch`,
|
|
365
|
+
LOG_LEVEL_NOTICE
|
|
366
|
+
);
|
|
367
|
+
await this.conflict.queueCheckFor(file.path);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
const storedRevision = await this.db.storeDeletionWithBaseRevision(file.path, baseRevision);
|
|
371
|
+
if (storedRevision === false) return false;
|
|
372
|
+
await this.deleteProvenance(file.path);
|
|
373
|
+
await this.conflict.queueCheckFor(file.path);
|
|
374
|
+
return true;
|
|
180
375
|
}
|
|
181
|
-
|
|
376
|
+
const deleted = await this.db.delete(file);
|
|
377
|
+
if (deleted) await this.deleteProvenance(file.path);
|
|
378
|
+
return deleted;
|
|
182
379
|
}
|
|
183
380
|
async renameFileInDB(info, oldPath) {
|
|
184
381
|
const newPath = getStoragePathFromUXFileInfo(info);
|
|
@@ -188,9 +385,17 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
188
385
|
]);
|
|
189
386
|
if (oldDocumentId === newDocumentId) {
|
|
190
387
|
this._log(`Updating the stored path for case-only rename: ${oldPath} -> ${newPath}`, LOG_LEVEL_VERBOSE);
|
|
191
|
-
return await this.
|
|
388
|
+
return await this.storeFileToDBFromRevision(info, true, false, oldPath);
|
|
192
389
|
}
|
|
193
390
|
const oldEntry = await this.db.fetchEntryMeta(oldPath, void 0, true);
|
|
391
|
+
const newEntry = await this.db.fetchEntryMeta(newPath, void 0, true);
|
|
392
|
+
if (newEntry && !newEntry.deleted && !newEntry._deleted) {
|
|
393
|
+
this._log(
|
|
394
|
+
`Refusing to overwrite the existing database entry while renaming ${oldPath} to ${newPath}`,
|
|
395
|
+
LOG_LEVEL_NOTICE
|
|
396
|
+
);
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
194
399
|
if (!await this.storeFileToDB(info, true)) {
|
|
195
400
|
this._log(`Failed to store rename target; preserving source in the database: ${oldPath}`, LOG_LEVEL_NOTICE);
|
|
196
401
|
return false;
|
|
@@ -199,54 +404,107 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
199
404
|
this._log(`Rename source is not present in the database: ${oldPath}`, LOG_LEVEL_VERBOSE);
|
|
200
405
|
return true;
|
|
201
406
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
407
|
+
const oldConflicts = await this.db.getConflictedRevs(oldPath);
|
|
408
|
+
if (oldConflicts.length > 0) {
|
|
409
|
+
let baseRevision = (await this.getProvenance(oldPath))?.revision;
|
|
410
|
+
if (!baseRevision) {
|
|
411
|
+
const readFile = await this.readFileFromStub(info);
|
|
412
|
+
const revisions = await this.db.findContentRevisions(oldPath, readFile.body);
|
|
413
|
+
baseRevision = revisions.length === 1 ? revisions[0] : void 0;
|
|
414
|
+
}
|
|
415
|
+
if (!baseRevision) {
|
|
416
|
+
this._log(
|
|
417
|
+
`Renamed ${oldPath} to ${newPath}, but preserved every conflicted source branch because the displayed source revision is unknown`,
|
|
418
|
+
LOG_LEVEL_NOTICE
|
|
419
|
+
);
|
|
420
|
+
await this.conflict.queueCheckFor(oldPath);
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
const storedRevision = await this.db.storeDeletionWithBaseRevision(
|
|
424
|
+
oldPath,
|
|
425
|
+
baseRevision
|
|
426
|
+
);
|
|
427
|
+
if (storedRevision === false) return false;
|
|
428
|
+
await this.deleteProvenance(oldPath);
|
|
429
|
+
await this.conflict.queueCheckFor(oldPath);
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
const deleted = await this.db.delete(oldPath);
|
|
433
|
+
if (deleted) await this.deleteProvenance(oldPath);
|
|
434
|
+
return deleted;
|
|
209
435
|
}
|
|
210
436
|
async deleteRevisionFromDB(info, rev) {
|
|
211
|
-
|
|
437
|
+
const path = getStoragePathFromUXFileInfo(info);
|
|
438
|
+
const provenance = await this.getProvenance(path);
|
|
439
|
+
const deleted = await this.db.delete(info, rev);
|
|
440
|
+
if (deleted && provenance?.revision === rev) {
|
|
441
|
+
await this.deleteProvenance(path);
|
|
442
|
+
}
|
|
443
|
+
return deleted;
|
|
212
444
|
}
|
|
213
445
|
async resolveConflictedByDeletingRevision(info, rev) {
|
|
214
446
|
const path = getStoragePathFromUXFileInfo(info);
|
|
447
|
+
const file = await this.infoToStub(info);
|
|
448
|
+
const docEntry = await this.db.fetchEntryMeta(file ?? info, rev, true);
|
|
449
|
+
if (!docEntry) {
|
|
450
|
+
this._log(`Failed to read the conflicted revision ${rev} of ${path}`, LOG_LEVEL_VERBOSE);
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
215
453
|
if (!await this.deleteRevisionFromDB(info, rev)) {
|
|
216
454
|
this._log(`Failed to delete the conflicted revision ${rev} of ${path}`, LOG_LEVEL_VERBOSE);
|
|
217
455
|
return false;
|
|
218
456
|
}
|
|
219
|
-
if (!await this.
|
|
457
|
+
if (!await this.applyDatabaseEntryToStorage(docEntry, file, true)) {
|
|
220
458
|
this._log(`Failed to apply the resolved revision ${rev} of ${path} to the storage`, LOG_LEVEL_VERBOSE);
|
|
221
459
|
return false;
|
|
222
460
|
}
|
|
461
|
+
await this.deleteProvenance(path);
|
|
223
462
|
}
|
|
224
463
|
async dbToStorageWithSpecificRev(info, rev, force) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
this._log(`File ${tryGetFilePath(info)} is not exist on the storage`, LOG_LEVEL_VERBOSE);
|
|
464
|
+
if (info == null) {
|
|
465
|
+
this._log(`Cannot select database revision ${rev} without a file path`, LOG_LEVEL_VERBOSE);
|
|
228
466
|
return false;
|
|
229
467
|
}
|
|
230
|
-
const
|
|
468
|
+
const file = await this.infoToStub(info);
|
|
469
|
+
const databaseTarget = file ?? info;
|
|
470
|
+
const [docEntry, currentEntry, conflictedRevisions] = await Promise.all([
|
|
471
|
+
this.db.fetchEntryMeta(databaseTarget, rev, true),
|
|
472
|
+
this.db.fetchEntryMeta(databaseTarget, void 0, true),
|
|
473
|
+
this.db.getConflictedRevs(databaseTarget)
|
|
474
|
+
]);
|
|
231
475
|
if (!docEntry) {
|
|
232
|
-
this._log(`File ${
|
|
476
|
+
this._log(`File ${tryGetFilePath(info)} is not exist on the database`, LOG_LEVEL_VERBOSE);
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
const liveRevisions = /* @__PURE__ */ new Set([
|
|
480
|
+
...currentEntry && currentEntry._rev ? [currentEntry._rev] : [],
|
|
481
|
+
...conflictedRevisions
|
|
482
|
+
]);
|
|
483
|
+
if (!liveRevisions.has(rev)) {
|
|
484
|
+
this._log(
|
|
485
|
+
`Could not apply ${tryGetFilePath(info)} revision ${rev}; the selected revision is no longer live`,
|
|
486
|
+
LOG_LEVEL_NOTICE
|
|
487
|
+
);
|
|
233
488
|
return false;
|
|
234
489
|
}
|
|
235
|
-
return await this.
|
|
490
|
+
return await this.applyDatabaseEntryToStorage(docEntry, file, force, true);
|
|
236
491
|
}
|
|
237
492
|
async dbToStorage(entryInfo, info, force) {
|
|
238
493
|
const file = await this.infoToStub(info);
|
|
239
|
-
const mode = file == null ? "create" : "modify";
|
|
240
494
|
const pathFromEntryInfo = typeof entryInfo === "string" ? entryInfo : this.getPath(entryInfo);
|
|
241
495
|
const docEntry = await this.db.fetchEntryMeta(pathFromEntryInfo, void 0, true);
|
|
242
496
|
if (!docEntry) {
|
|
243
497
|
this._log(`File ${pathFromEntryInfo} is not exist on the database`, LOG_LEVEL_VERBOSE);
|
|
244
498
|
return false;
|
|
245
499
|
}
|
|
500
|
+
return await this.applyDatabaseEntryToStorage(docEntry, file, force);
|
|
501
|
+
}
|
|
502
|
+
async applyDatabaseEntryToStorage(docEntry, file, force, allowExistingConflicts = false) {
|
|
503
|
+
const mode = file == null ? "create" : "modify";
|
|
246
504
|
const path = this.getPath(docEntry);
|
|
247
505
|
const settings = this.setting.currentSettings();
|
|
248
506
|
const revs = await this.db.getConflictedRevs(path);
|
|
249
|
-
if (revs.length > 0) {
|
|
507
|
+
if (revs.length > 0 && !allowExistingConflicts) {
|
|
250
508
|
if (settings.writeDocumentsIfConflicted) {
|
|
251
509
|
} else {
|
|
252
510
|
await this.conflict.queueCheckForIfOpen(path);
|
|
@@ -261,6 +519,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
261
519
|
const existOnDB = !(docEntry._deleted || docEntry.deleted || false);
|
|
262
520
|
if (!existOnDB && !existDoc) {
|
|
263
521
|
this._log(`File ${path} seems to be deleted, but already not on storage`, LOG_LEVEL_VERBOSE);
|
|
522
|
+
await this.deleteProvenance(path);
|
|
264
523
|
return true;
|
|
265
524
|
}
|
|
266
525
|
if (!existOnDB && existDoc) {
|
|
@@ -268,6 +527,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
268
527
|
return true;
|
|
269
528
|
}
|
|
270
529
|
await this.storage.deleteVaultItem(path);
|
|
530
|
+
await this.deleteProvenance(path);
|
|
271
531
|
return true;
|
|
272
532
|
}
|
|
273
533
|
if (existDoc && existDoc.path !== path) {
|
|
@@ -294,6 +554,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
294
554
|
this._log(`Could not apply the stored filename case: ${existDoc.path} -> ${path}`, LOG_LEVEL_NOTICE);
|
|
295
555
|
return false;
|
|
296
556
|
}
|
|
557
|
+
await this.deleteProvenance(existDoc.path);
|
|
297
558
|
existDoc = renamedFile;
|
|
298
559
|
}
|
|
299
560
|
const docRead = await this.db.fetchEntryFromMeta(docEntry);
|
|
@@ -311,6 +572,14 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
311
572
|
}
|
|
312
573
|
}
|
|
313
574
|
const docData = readContent(docRead);
|
|
575
|
+
if (allowExistingConflicts && existDoc && !force) {
|
|
576
|
+
const readFile = await this.readFileFromStub(existDoc);
|
|
577
|
+
if (await isDocContentSame(docData, readFile.body)) {
|
|
578
|
+
await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime);
|
|
579
|
+
this.path.markChangesAreSame(docRead, docRead.mtime, existDoc.stat.mtime);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
314
583
|
if (existDoc && !force) {
|
|
315
584
|
let shouldApplied = false;
|
|
316
585
|
const freshness = this.path.compareFileFreshness(existDoc, docEntry);
|
|
@@ -327,6 +596,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
327
596
|
}
|
|
328
597
|
}
|
|
329
598
|
if (!shouldApplied) {
|
|
599
|
+
await this.setProvenance(path, docEntry._rev, existDoc.stat.mtime);
|
|
330
600
|
this._log(`File ${docRead.path} is not changed`, LOG_LEVEL_VERBOSE);
|
|
331
601
|
return true;
|
|
332
602
|
}
|
|
@@ -343,6 +613,10 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
343
613
|
const ret = await this.storage.writeFileAuto(path, docData, { ctime: docRead.ctime, mtime: docRead.mtime });
|
|
344
614
|
await this.storage.touched(path);
|
|
345
615
|
this.storage.triggerFileEvent(mode, path);
|
|
616
|
+
if (ret && this.fileReflectionProvenance) {
|
|
617
|
+
const storedStat = await this.storage.stat(path);
|
|
618
|
+
await this.setProvenance(path, docEntry._rev, storedStat?.mtime);
|
|
619
|
+
}
|
|
346
620
|
return ret;
|
|
347
621
|
}
|
|
348
622
|
async preserveUnsyncedStorageAsConflict(path, existDoc, incomingEntry, incomingContent) {
|
|
@@ -359,11 +633,12 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
359
633
|
if (await this.db.hasContentInRevisionHistory(path, readFile.body, incomingEntry._rev)) {
|
|
360
634
|
return false;
|
|
361
635
|
}
|
|
362
|
-
const
|
|
363
|
-
if (
|
|
636
|
+
const storedRevision = await this.db.storeAsConflictedRevisionWithResult(readFile, incomingEntry._rev, true);
|
|
637
|
+
if (storedRevision === false) {
|
|
364
638
|
this._log(`Prevented overwriting unsynchronised local changes for ${path}`, LOG_LEVEL_NOTICE);
|
|
365
639
|
return true;
|
|
366
640
|
}
|
|
641
|
+
await this.setProvenance(path, storedRevision, readFile.stat.mtime);
|
|
367
642
|
this._log(`Preserved unsynchronised local changes as a conflict for ${path}`, LOG_LEVEL_NOTICE);
|
|
368
643
|
await this.conflict.queueCheckFor(path);
|
|
369
644
|
return true;
|
|
@@ -37,6 +37,6 @@ export declare abstract class KeyValueDBService<T extends ServiceContext = Servi
|
|
|
37
37
|
set: (key: string, value: unknown) => Promise<void>;
|
|
38
38
|
delete: (key: string) => Promise<void>;
|
|
39
39
|
keys: (from: string | undefined, to: string | undefined, count?: number) => Promise<string[]>;
|
|
40
|
-
db: Promise<KeyValueDatabase>;
|
|
40
|
+
readonly db: Promise<KeyValueDatabase>;
|
|
41
41
|
};
|
|
42
42
|
}
|
|
@@ -124,7 +124,9 @@ class KeyValueDBService extends ServiceBase {
|
|
|
124
124
|
);
|
|
125
125
|
return ret.map((e) => (typeof e === "string" ? e : JSON.stringify(e)).toString()).filter((e) => e.startsWith(prefix)).map((e) => e.substring(prefix.length));
|
|
126
126
|
},
|
|
127
|
-
db
|
|
127
|
+
get db() {
|
|
128
|
+
return Promise.resolve(getDB());
|
|
129
|
+
}
|
|
128
130
|
};
|
|
129
131
|
}
|
|
130
132
|
}
|