dsh-webdav-sync 1.0.4 → 1.0.5
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/lib/engine.js +54 -16
- package/lib/index.js +8 -7
- package/lib/wire.d.ts +2 -0
- package/package.json +1 -1
package/lib/engine.js
CHANGED
|
@@ -164,6 +164,11 @@ async function syncOnce(engine, state, force) {
|
|
|
164
164
|
state.blind = undefined;
|
|
165
165
|
}
|
|
166
166
|
const snapshot = await take(paths, config, engine.credentials);
|
|
167
|
+
if (snapshot.keysProblems.length === 0 && state.local?.keys !== undefined) {
|
|
168
|
+
// Older state files predate the explicit marker; an existing keys hash is
|
|
169
|
+
// already a trustworthy baseline, even before this run measures it again.
|
|
170
|
+
state.keysBaseline = true;
|
|
171
|
+
}
|
|
167
172
|
let remote = await openRemote(engine, state, read);
|
|
168
173
|
const first = state.local === undefined;
|
|
169
174
|
// A directory could not be read this run: its part stands in for the server's
|
|
@@ -175,7 +180,11 @@ async function syncOnce(engine, state, force) {
|
|
|
175
180
|
&& sameHashes(hashesOf(partsOf(snapshot, remote.part('dirs'))), state.local)
|
|
176
181
|
&& !force) {
|
|
177
182
|
// Neither side moved: the last sync is still the state of the world.
|
|
178
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
here: [], there: [], pushed: false, reloadNeeded: false,
|
|
185
|
+
...keyProblems(snapshot.keysProblems),
|
|
186
|
+
...(await pendings(engine, snapshot.parts.plugins)),
|
|
187
|
+
};
|
|
179
188
|
}
|
|
180
189
|
if (remote.notModified) {
|
|
181
190
|
// The server said the file is the one last read, which was worth knowing for
|
|
@@ -185,6 +194,10 @@ async function syncOnce(engine, state, force) {
|
|
|
185
194
|
}
|
|
186
195
|
const carried = remote.part('dirs');
|
|
187
196
|
const localParts = partsOf(snapshot, carried);
|
|
197
|
+
// A failed credential read is unknown, not a deletion or a partial replacement.
|
|
198
|
+
// Defer this part while the other parts sync, carrying the server's copy below.
|
|
199
|
+
if (snapshot.keysProblems.length > 0)
|
|
200
|
+
delete localParts.keys;
|
|
188
201
|
const local = hashesOf(localParts);
|
|
189
202
|
// The server holds no file of ours. This computer's setup becomes the first
|
|
190
203
|
// version, which is what joining an empty folder means.
|
|
@@ -206,10 +219,10 @@ async function syncOnce(engine, state, force) {
|
|
|
206
219
|
for (const part of PARTS) {
|
|
207
220
|
const remoteHas = remote.hashes[part] !== undefined;
|
|
208
221
|
const remotePart = () => remote.part(part);
|
|
209
|
-
if (!syncsPart(config, part)) {
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
222
|
+
if (!syncsPart(config, part) || (part === 'keys' && snapshot.keysProblems.length > 0)) {
|
|
223
|
+
// Disabled or unreadable: carry the server's copy untouched. In particular,
|
|
224
|
+
// a missing/locked credential service must not look like local key deletion
|
|
225
|
+
// and erase the keys already uploaded by a successful manual sync.
|
|
213
226
|
if (remoteHas)
|
|
214
227
|
merged[part] = remotePart();
|
|
215
228
|
continue;
|
|
@@ -218,7 +231,8 @@ async function syncOnce(engine, state, force) {
|
|
|
218
231
|
// the last sync — not merely when its presence changed. Comparing presence
|
|
219
232
|
// would read an edit made only on the other computer as "unchanged" and
|
|
220
233
|
// quietly keep this computer's older copy.
|
|
221
|
-
const
|
|
234
|
+
const missingKeyBaseline = !first && state.keysBaseline !== true && part === 'keys';
|
|
235
|
+
const changedHere = !first && !missingKeyBaseline && local[part] !== state.local?.[part];
|
|
222
236
|
const changedThere = first || remote.hashes[part] !== state.remote?.[part];
|
|
223
237
|
// A part the other computer took away is taken away here too — but never the
|
|
224
238
|
// plugin list, which is what loads the shell, and never on a first sync,
|
|
@@ -327,11 +341,12 @@ async function syncOnce(engine, state, force) {
|
|
|
327
341
|
const pushNeeded = force || remote.rebuilt === true || !sameHashes(mergedHashes, remote.hashes);
|
|
328
342
|
if (!pushNeeded) {
|
|
329
343
|
const written = await applyMerged(engine, snapshot, applied);
|
|
330
|
-
state.remote = remote.hashes;
|
|
331
344
|
// Read back rather than assume: a directory whose file was just written — or
|
|
332
345
|
// removed — has to be hashed as it now is, or the next sync would see an edit
|
|
333
|
-
// that this one made itself and call it a conflict.
|
|
334
|
-
|
|
346
|
+
// that this one made itself and call it a conflict. Keep the key baselines if
|
|
347
|
+
// this read could not measure them, so recovery still sees pending edits.
|
|
348
|
+
const after = await take(paths, config, engine.credentials);
|
|
349
|
+
recordHashes(state, hashesOf(partsOf(after, undefined)), remote.hashes, snapshot.keysProblems.length === 0 && after.keysProblems.length === 0);
|
|
335
350
|
// A part can come in without the file needing to be written: when only the
|
|
336
351
|
// server moved, the merge agrees with it. The person still has to be told
|
|
337
352
|
// what changed here, so the notice is recorded on this path too — clearing
|
|
@@ -351,7 +366,7 @@ async function syncOnce(engine, state, force) {
|
|
|
351
366
|
...(saved === undefined ? {} : { saved }),
|
|
352
367
|
pushed: false,
|
|
353
368
|
reloadNeeded: written.reloadNeeded,
|
|
354
|
-
...keyProblems(written.keysProblems),
|
|
369
|
+
...keyProblems([...snapshot.keysProblems, ...written.keysProblems]),
|
|
355
370
|
...(await pendings(engine, applied.plugins)),
|
|
356
371
|
};
|
|
357
372
|
}
|
|
@@ -556,11 +571,11 @@ async function push(engine, state, snapshot, merged) {
|
|
|
556
571
|
// failed upload never leaves this computer holding another computer's setup
|
|
557
572
|
// with nothing to say where its own went.
|
|
558
573
|
const applied = await applyMerged(engine, snapshot, merged.applied);
|
|
559
|
-
const
|
|
574
|
+
const after = await take(paths, config, engine.credentials);
|
|
575
|
+
const now = hashesOf(partsOf(after, undefined));
|
|
560
576
|
state.server = written;
|
|
561
577
|
state.sum = sum(sealed);
|
|
562
|
-
state
|
|
563
|
-
state.local = now;
|
|
578
|
+
recordHashes(state, now, hashesOf(bundle), snapshot.keysProblems.length === 0 && after.keysProblems.length === 0);
|
|
564
579
|
state.blind = written.etag === undefined || written.etag === '' ? { ...before } : undefined;
|
|
565
580
|
state.undo = undefined;
|
|
566
581
|
const notice = merged.here.length > 0 || merged.there.length > 0
|
|
@@ -579,7 +594,7 @@ async function push(engine, state, snapshot, merged) {
|
|
|
579
594
|
...(merged.saved === undefined ? {} : { saved: merged.saved }),
|
|
580
595
|
pushed: true,
|
|
581
596
|
reloadNeeded: applied.reloadNeeded,
|
|
582
|
-
...keyProblems(applied.keysProblems),
|
|
597
|
+
...keyProblems([...snapshot.keysProblems, ...applied.keysProblems]),
|
|
583
598
|
...(await pendings(engine, merged.merged.plugins)),
|
|
584
599
|
};
|
|
585
600
|
}
|
|
@@ -664,6 +679,29 @@ async function applyMerged(engine, snapshot, applied) {
|
|
|
664
679
|
}
|
|
665
680
|
return { reloadNeeded, keysProblems };
|
|
666
681
|
}
|
|
682
|
+
/** Record measured parts without turning an unreadable key store into a baseline. */
|
|
683
|
+
function recordHashes(state, local, remote, keysReadable) {
|
|
684
|
+
if (keysReadable)
|
|
685
|
+
state.keysBaseline = true;
|
|
686
|
+
if (!keysReadable) {
|
|
687
|
+
// Keep both comparison points: when the service returns, local and remote
|
|
688
|
+
// key edits still need to be merged rather than accepted as already applied.
|
|
689
|
+
if (state.local?.keys === undefined)
|
|
690
|
+
delete local.keys;
|
|
691
|
+
else
|
|
692
|
+
local.keys = state.local.keys;
|
|
693
|
+
remote = { ...remote };
|
|
694
|
+
if (state.remote?.keys === undefined)
|
|
695
|
+
delete remote.keys;
|
|
696
|
+
else
|
|
697
|
+
remote.keys = state.remote.keys;
|
|
698
|
+
// The server may have changed keys during this attempt. A conditional 304
|
|
699
|
+
// would hide its body on recovery even though those keys are still pending.
|
|
700
|
+
state.server = undefined;
|
|
701
|
+
}
|
|
702
|
+
state.local = local;
|
|
703
|
+
state.remote = remote;
|
|
704
|
+
}
|
|
667
705
|
/**
|
|
668
706
|
* The `keysProblems` field of a result, present only when there is one.
|
|
669
707
|
*
|
|
@@ -838,7 +876,7 @@ export async function restore(engine) {
|
|
|
838
876
|
...(kept === undefined ? {} : { saved: keptDir(paths) }),
|
|
839
877
|
pushed: false,
|
|
840
878
|
reloadNeeded: written.reloadNeeded,
|
|
841
|
-
...keyProblems(written.keysProblems),
|
|
879
|
+
...keyProblems([...snapshot.keysProblems, ...written.keysProblems]),
|
|
842
880
|
...(await pendings(engine, applied.plugins)),
|
|
843
881
|
};
|
|
844
882
|
});
|
|
@@ -939,7 +977,7 @@ export async function undo(engine) {
|
|
|
939
977
|
there: [],
|
|
940
978
|
pushed: false,
|
|
941
979
|
reloadNeeded: written.reloadNeeded,
|
|
942
|
-
...keyProblems(written.keysProblems),
|
|
980
|
+
...keyProblems([...snapshot.keysProblems, ...written.keysProblems]),
|
|
943
981
|
...(await pendings(engine, merged.plugins)),
|
|
944
982
|
};
|
|
945
983
|
});
|
package/lib/index.js
CHANGED
|
@@ -348,7 +348,7 @@ export function apply(ctx, config = {}) {
|
|
|
348
348
|
send(res, 200, await installPlugins(current.paths));
|
|
349
349
|
});
|
|
350
350
|
});
|
|
351
|
-
startTimer(ctx,
|
|
351
|
+
startTimer(ctx, engine);
|
|
352
352
|
}
|
|
353
353
|
/** Make sure the state directory exists, so the first sync has somewhere to write. */
|
|
354
354
|
async function saveStateIfUsable(engine) {
|
|
@@ -368,7 +368,7 @@ async function saveStateIfUsable(engine) {
|
|
|
368
368
|
* A failure is recorded in the state by `sync` itself and changes nothing here:
|
|
369
369
|
* the next tick tries again, which is the whole point of a timer.
|
|
370
370
|
*/
|
|
371
|
-
function startTimer(ctx,
|
|
371
|
+
function startTimer(ctx, currentEngine) {
|
|
372
372
|
let timer;
|
|
373
373
|
let stopped = false;
|
|
374
374
|
const tick = async () => {
|
|
@@ -376,11 +376,12 @@ function startTimer(ctx, currentConfig) {
|
|
|
376
376
|
return;
|
|
377
377
|
let wait = 0;
|
|
378
378
|
try {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
379
|
+
// Use the same fresh engine as the routes: reading only sync.json would
|
|
380
|
+
// drop both the host credential service and a session-only WebDAV password.
|
|
381
|
+
const current = await currentEngine();
|
|
382
|
+
const minutes = current.config.autoEvery;
|
|
383
|
+
if (usable(current.config) && minutes > 0) {
|
|
384
|
+
await sync(current).catch(() => undefined);
|
|
384
385
|
wait = minutes * 60_000;
|
|
385
386
|
}
|
|
386
387
|
}
|
package/lib/wire.d.ts
CHANGED
|
@@ -292,6 +292,8 @@ export interface StoredState extends SyncRecord {
|
|
|
292
292
|
key?: string | undefined;
|
|
293
293
|
/** Last sync attempt, ISO-8601. */
|
|
294
294
|
last?: string | undefined;
|
|
295
|
+
/** True once keys have a trustworthy local comparison baseline. */
|
|
296
|
+
keysBaseline?: boolean | undefined;
|
|
295
297
|
/**
|
|
296
298
|
* Last attempt that actually moved something, ISO-8601: a push, a part taken
|
|
297
299
|
* from the server, or a restore. Absent until one has. This is the difference
|
package/package.json
CHANGED