diff --git a/chrome/content/zotero/xpcom/sync/syncEngine.js b/chrome/content/zotero/xpcom/sync/syncEngine.js index 7dc6acc76..edf413a3c 100644 --- a/chrome/content/zotero/xpcom/sync/syncEngine.js +++ b/chrome/content/zotero/xpcom/sync/syncEngine.js @@ -325,6 +325,7 @@ Zotero.Sync.Data.Engine.prototype._startDownload = Zotero.Promise.coroutine(func let objectType = Zotero.DataObjectUtilities.getObjectTypeSingular(objectTypePlural); let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); let toDelete = []; + let conflicts = []; for (let key of results.deleted[objectTypePlural]) { // TODO: Remove from request? if (objectType == 'tag') { @@ -357,11 +358,55 @@ Zotero.Sync.Data.Engine.prototype._startDownload = Zotero.Promise.coroutine(func } // Conflict resolution else if (objectType == 'item') { - throw new Error("Unimplemented: delete conflict"); + conflicts.push({ + left: yield obj.toJSON(), + right: { + deleted: true + } + }); } // Ignore deletion if collection/search changed locally } + + if (conflicts.length) { + conflicts.sort(function (a, b) { + var d1 = a.left.dateModified; + var d2 = b.left.dateModified; + if (d1 > d2) { + return 1 + } + if (d1 < d2) { + return -1; + } + return 0; + }); + var mergeData = Zotero.Sync.Data.Local.resolveConflicts(conflicts); + if (mergeData) { + let concurrentObjects = 50; + yield Zotero.Utilities.Internal.forEachChunkAsync( + mergeData, + concurrentObjects, + function (chunk) { + return Zotero.DB.executeTransaction(function* () { + for (let json of chunk) { + if (!json.deleted) continue; + let obj = yield objectsClass.getByLibraryAndKeyAsync( + this.libraryID, json.key, { noCache: true } + ); + if (!obj) { + Zotero.logError("Remotely deleted " + objectType + + " didn't exist after conflict resolution"); + continue; + } + yield obj.erase(); + } + }.bind(this)); + }.bind(this) + ); + } + } + if (toDelete.length) { yield Zotero.DB.executeTransaction(function* () { for (let obj of toDelete) { diff --git a/test/tests/syncEngineTest.js b/test/tests/syncEngineTest.js index 895d552c5..20a656334 100644 --- a/test/tests/syncEngineTest.js +++ b/test/tests/syncEngineTest.js @@ -741,6 +741,88 @@ describe("Zotero.Sync.Data.Engine", function () { assert.ok(Zotero.Collections.exists(collectionID)); assert.ok(Zotero.Searches.exists(searchID)); }) + + it("should show conflict resolution window for conflicting remote deletions", function* () { + var userLibraryID = Zotero.Libraries.userLibraryID; + yield Zotero.Libraries.setVersion(userLibraryID, 5); + ({ engine, client, caller } = yield setup()); + + // Create local unsynced items + var item = createUnsavedDataObject('item'); + item.setField('title', 'A'); + item.synced = false; + var itemID1 = yield item.saveTx({ skipSyncedUpdate: true }); + var itemKey1 = item.key; + + item = createUnsavedDataObject('item'); + item.setField('title', 'B'); + item.synced = false; + var itemID2 = yield item.saveTx({ skipSyncedUpdate: true }); + var itemKey2 = item.key; + + var headers = { + "Last-Modified-Version": 6 + }; + setResponse({ + method: "GET", + url: "users/1/settings?since=5", + status: 200, + headers: headers, + json: {} + }); + setResponse({ + method: "GET", + url: "users/1/collections?format=versions&since=5", + status: 200, + headers: headers, + json: {} + }); + setResponse({ + method: "GET", + url: "users/1/searches?format=versions&since=5", + status: 200, + headers: headers, + json: {} + }); + setResponse({ + method: "GET", + url: "users/1/items?format=versions&since=5&includeTrashed=1", + status: 200, + headers: headers, + json: {} + }); + setResponse({ + method: "GET", + url: "users/1/deleted?since=5", + status: 200, + headers: headers, + json: { + settings: [], + collections: [], + searches: [], + items: [itemKey1, itemKey2] + } + }); + + waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { + var doc = dialog.document; + var wizard = doc.documentElement; + var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; + + // 1 (accept remote deletion) + assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true'); + mergeGroup.rightpane.click(); + wizard.getButton('next').click(); + + // 2 (ignore remote deletion) + assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true'); + wizard.getButton('finish').click(); + }) + yield engine._startDownload(); + + assert.isFalse(Zotero.Items.exists(itemID1)); + assert.isTrue(Zotero.Items.exists(itemID2)); + }) }) describe("#_upgradeCheck()", function () {