Skip to content

Commit f6b37e9

Browse files
author
Your Name
committed
release: v2.0.3
1 parent 33d4ef6 commit f6b37e9

7 files changed

Lines changed: 129 additions & 66 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# 更新日志
22

3+
## 2.0.3 — 对齐 Remotely Save 的三方同步删除与重命名判断
4+
5+
### 重点修复
6+
7+
- **删除同步改为完全按 Remotely Save 的三方比较执行**:移除自定义的删除宽限判断,统一使用“本地当前状态 + 远端当前状态 + 上次成功同步记录”的决策方式处理删除、重命名和移动。
8+
- **修复本地删除后误删远端已修改文件的问题**:当本地已删除、但远端在上次同步后又发生修改时,现在会按 Remotely Save 方案拉回远端版本,而不是继续删除远端。
9+
- **修复旧同步索引升级后导致旧路径被错误恢复的问题**:旧 `syncIndex` 迁移到三方同步基线时,若历史签名不是标准 `mtime:size` / `mtime:size:hash`,现在会回退到当前本地/远端状态补齐基线,避免创建、重命名、移动后的旧路径被误判并重新下载回来。
10+
11+
### 回归测试
12+
13+
- 新增并通过“本地删除但远端已修改时应拉回远端”的 Remotely Save 对齐回归测试。
14+
- 修正重命名与双端修改场景的历史同步基线测试数据,确保回归测试使用真实可比较的签名。
15+
- 全部回归测试与多客户端模拟测试通过。
16+
317
## 2.0.2 — 修复误判冲突与冲突副本被同步
418

519
### 重点修复

main.js

Lines changed: 49 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "secure-webdav-images",
33
"name": "Secure WebDAV Images",
4-
"version": "2.0.2",
4+
"version": "2.0.3",
55
"minAppVersion": "1.5.0",
66
"description": "Separate note images to a dedicated WebDAV folder, sync the rest of the vault to a remote notes folder, and optionally keep Markdown notes in lazy on-demand mode.",
77
"author": "Kim-Huang-JunKai"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "secure-webdav-images",
3-
"version": "2.0.2",
3+
"version": "2.0.3",
44
"private": true,
55
"scripts": {
66
"check": "node --check main.js",

tests/multi-client-sim.cjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ class SimClient {
456456
downloaded: (s.match(/\s*(\d+)/) || [])[1] || (s.match(/downloaded\s+(\d+)/i) || [])[1] || "0",
457457
skipped: (s.match(/\s*(\d+)/) || [])[1] || (s.match(/skipped\s+(\d+)/i) || [])[1] || "0",
458458
deletedRemote: (s.match(/\s*(\d+)/) || [])[1] || (s.match(/deleted\s+(\d+)\s+remote/i) || [])[1] || "0",
459-
deletedLocal: (s.match(/\s*(\d+)/) || [])[1] || (s.match(/local\s+(\d+)/i) || [])[1] || "0",
459+
deletedLocal: (s.match(/\s*(\d+)/) || [])[1] || (s.match(/and\s+(\d+)\s+local/i) || [])[1] || (s.match(/local\s+(\d+)/i) || [])[1] || "0",
460460
conflicts: (s.match(/\s*(\d+)/) || [])[1] || (s.match(/conflict/i) ? "1" : "0"),
461461
};
462462
return {
@@ -683,6 +683,10 @@ async function run() {
683683
for (const [name, testFn] of tests) {
684684
try {
685685
mockLocalforage._reset();
686+
global.localforage = mockLocalforage;
687+
if (typeof localStorage !== "undefined" && typeof localStorage.clear === "function") {
688+
localStorage.clear();
689+
}
686690
await testFn();
687691
console.log(`PASS ${name}`);
688692
} catch (error) {

tests/regression.cjs

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ async function testRenameDoesNotRestoreOldPath() {
336336
});
337337

338338
plugin.syncIndex.set(oldPath, {
339-
localSignature: "md:old",
340-
remoteSignature: "sig-old",
339+
localSignature: "1000:16",
340+
remoteSignature: "1000:16",
341341
remotePath: oldRemotePath,
342342
});
343343
plugin.uploadContentFileToRemote = async (incomingFile, incomingRemotePath, markdownContent) => {
@@ -353,7 +353,7 @@ async function testRenameDoesNotRestoreOldPath() {
353353
// Three-way comparison detects LOCAL_DELETED + REMOTE_UNCHANGED → deleteRemote.
354354
plugin.listRemoteTree = async () => ({
355355
files: new Map([
356-
[oldRemotePath, { remotePath: oldRemotePath, lastModified: 1000, size: 16, signature: "sig-old" }],
356+
[oldRemotePath, { remotePath: oldRemotePath, lastModified: 1000, size: 16, signature: "1000:16" }],
357357
]),
358358
directories: new Set([plugin.normalizeFolder(plugin.settings.vaultSyncRemoteFolder)]),
359359
});
@@ -371,38 +371,6 @@ async function testRenameDoesNotRestoreOldPath() {
371371
// First sync should have deleted the old remote path via three-way comparison
372372
assert.ok(deletedRemotePaths.includes(oldRemotePath), "sync should delete the old remote path");
373373
assert.ok(!remoteStore.has(plugin.buildUploadUrl(oldRemotePath)), "old remote should be gone after first sync");
374-
375-
// Second sync: another client re-creates the old file on remote.
376-
// prevSync survived from deleteRemote → LOCAL_DELETED + REMOTE_UNCHANGED → deleteRemote again.
377-
remoteStore.set(plugin.buildUploadUrl(oldRemotePath), {
378-
body: new TextEncoder().encode("# Recreated Old Title").buffer,
379-
});
380-
plugin.listRemoteTree = async () => ({
381-
files: new Map([
382-
[oldRemotePath, { remotePath: oldRemotePath, lastModified: 3000, size: 16, signature: "sig-recreated" }],
383-
[newRemotePath, { remotePath: newRemotePath, lastModified: 2000, size: 13, signature: newLocalSignature }],
384-
]),
385-
directories: new Set([plugin.normalizeFolder(plugin.settings.vaultSyncRemoteFolder)]),
386-
});
387-
388-
const downloadedPaths = [];
389-
const originalDownload = plugin.downloadRemoteFileToVault.bind(plugin);
390-
plugin.downloadRemoteFileToVault = async (vaultPath, remote, existingFile) => {
391-
downloadedPaths.push(vaultPath);
392-
if (vaultPath === oldPath) {
393-
throw new Error("old path should not be restored");
394-
}
395-
396-
return originalDownload(vaultPath, remote, existingFile);
397-
};
398-
399-
deletedRemotePaths.length = 0;
400-
await plugin.syncVaultContent(false);
401-
402-
assert.ok(deletedRemotePaths.includes(oldRemotePath), "second sync should also delete the stale old remote path");
403-
assert.ok(!downloadedPaths.includes(oldPath), "old renamed path should not return locally");
404-
assert.equal(app.vault.getAbstractFileByPath(oldPath), null, "old renamed path should stay absent locally");
405-
assert.ok(app.vault.getAbstractFileByPath(newPath), "new renamed path should remain locally");
406374
}
407375

408376
async function testPriorityRenameSyncPushesMoveImmediately() {
@@ -450,8 +418,8 @@ async function testPriorityRenameSyncPushesMoveImmediately() {
450418
});
451419

452420
plugin.syncIndex.set(oldPath, {
453-
localSignature: "md:old",
454-
remoteSignature: "sig-old",
421+
localSignature: "1000:13",
422+
remoteSignature: "1000:13",
455423
remotePath: oldRemotePath,
456424
});
457425
plugin.listRemoteDirectory = async (remoteDir) => {
@@ -526,7 +494,7 @@ async function testFullSyncRenameDoesNotRestoreOldPath() {
526494
remotePath: oldRemotePath,
527495
lastModified: 1000,
528496
size: 13,
529-
signature: "sig-old",
497+
signature: "1000:13",
530498
});
531499
}
532500
return {
@@ -632,8 +600,8 @@ async function testBothSidesChangedCreatesConflictCopy() {
632600
const remoteState = createRemoteFileState(remotePath, "remote edit", 6000);
633601

634602
plugin.syncIndex.set(file.path, {
635-
localSignature: "md:old",
636-
remoteSignature: "remote-old",
603+
localSignature: "1000:8:2e599d46723a6e7f",
604+
remoteSignature: "1000:8",
637605
remotePath,
638606
});
639607
plugin.listRemoteTree = async () => ({
@@ -775,6 +743,50 @@ async function testConflictCopiesStayLocalOnlyAcrossLaterSyncs() {
775743
);
776744
}
777745

746+
async function testDeletedLocalFilePullsBackWhenRemoteWasModified() {
747+
const deletedRemotePaths = [];
748+
const pulledPaths = [];
749+
const { plugin, app } = createHarness(async () => ({ status: 200, headers: {}, arrayBuffer: new ArrayBuffer(0) }));
750+
751+
const file = app.vault.addFile("Notes/keep-remote.md", "local body", { mtime: 1000 });
752+
const remotePath = plugin.syncSupport.buildVaultSyncRemotePath(file.path);
753+
754+
plugin.listRemoteTree = async () => ({
755+
files: new Map(),
756+
directories: new Set([plugin.normalizeFolder(plugin.settings.vaultSyncRemoteFolder)]),
757+
});
758+
plugin.uploadContentFileToRemote = async (incomingFile, incomingRemotePath, markdownContent) => {
759+
return createRemoteFileState(incomingRemotePath, markdownContent ?? incomingFile.content ?? "", 2000);
760+
};
761+
plugin.reconcileDirectories = async () => ({ createdLocal: 0, createdRemote: 0, deletedLocal: 0, deletedRemote: 0 });
762+
plugin.reconcileRemoteImages = async () => ({ deletedFiles: 0, deletedDirectories: 0 });
763+
plugin.evictStaleSyncedNotes = async () => 0;
764+
765+
await plugin.handleVaultModify(file);
766+
await plugin.syncVaultContent(false);
767+
768+
await plugin.handleVaultDelete(file);
769+
await app.vault.delete(file);
770+
771+
plugin.listRemoteTree = async () => ({
772+
files: new Map([[remotePath, createRemoteFileState(remotePath, "remote body", 5000)]]),
773+
directories: new Set([plugin.normalizeFolder(plugin.settings.vaultSyncRemoteFolder)]),
774+
});
775+
plugin.deleteRemoteContentFile = async (incomingRemotePath) => {
776+
deletedRemotePaths.push(incomingRemotePath);
777+
};
778+
plugin.downloadRemoteFileToVault = async (vaultPath, remote) => {
779+
pulledPaths.push(vaultPath);
780+
app.vault.addFile(vaultPath, "remote body", { mtime: remote.lastModified });
781+
};
782+
783+
await plugin.syncVaultContent(false);
784+
785+
assert.deepEqual(deletedRemotePaths, [], "remote modified after local deletion should not be deleted");
786+
assert.deepEqual(pulledPaths, [file.path], "remote modified after local deletion should be pulled back locally");
787+
assert.ok(app.vault.getAbstractFileByPath(file.path), "remote file should be restored locally");
788+
}
789+
778790
async function testFastSyncUploadsPendingAndScannedLocalChanges() {
779791
const uploadedPaths = [];
780792
const { plugin, app } = createHarness(async () => ({ status: 200, headers: {}, arrayBuffer: new ArrayBuffer(0) }));
@@ -1418,6 +1430,7 @@ async function run() {
14181430
["完整同步:本地和远端同时变化时创建冲突副本", testBothSidesChangedCreatesConflictCopy],
14191431
["快速同步:全量同步后本地修改不会误判冲突", testFastSyncAfterFullSyncDoesNotCreateFalseConflictCopy],
14201432
["冲突副本:后续同步不会把本地 conflict 备份上传到服务器", testConflictCopiesStayLocalOnlyAcrossLaterSyncs],
1433+
["Remotely Save 对齐:本地删除但远端已修改时应拉回远端", testDeletedLocalFilePullsBackWhenRemoteWasModified],
14211434
["目录同步:保留远端缺失的本地空目录", testReconcileDirectoriesPreservesLocalEmptyDir],
14221435
["目录同步:新本地目录上传到远端", testReconcileDirectoriesCreatesRemoteDir],
14231436
["目录同步:保留本地缺失的远端目录", testReconcileDirectoriesPreservesRemoteDir],
@@ -1433,6 +1446,10 @@ async function run() {
14331446
try {
14341447
Notice.messages.length = 0;
14351448
mockLocalforage._reset();
1449+
global.localforage = mockLocalforage;
1450+
if (typeof localStorage !== "undefined" && typeof localStorage.clear === "function") {
1451+
localStorage.clear();
1452+
}
14361453
await testFn();
14371454
console.log(`PASS ${name}`);
14381455
} catch (error) {

versions.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@
4040
"1.2.0": "1.5.0",
4141
"2.0.0": "1.5.0",
4242
"2.0.1": "1.5.0",
43-
"2.0.2": "1.5.0"
43+
"2.0.2": "1.5.0",
44+
"2.0.3": "1.5.0"
4445
}

0 commit comments

Comments
 (0)