commit f96341317004a14a1555e6b04d82885a133089ef
parent f8af231f1ab66810a6b540b78182fd14bc2cb864
Author: Dan Stillman <dstillman@zotero.org>
Date: Thu, 6 Aug 2015 06:03:17 -0400
Handle conflict resolution for remote item deletions
Diffstat:
2 files changed, 128 insertions(+), 1 deletion(-)
diff --git 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
@@ -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 () {