Add deletion uploading to API syncing [DB reupgrade]

Tags deletions are not currently synced, and maybe don't need to be.
This commit is contained in:
Dan Stillman 2015-11-01 03:43:04 -05:00
parent 6b8e5bafc6
commit 1e6c29766f
7 changed files with 421 additions and 205 deletions

View File

@ -2211,20 +2211,18 @@ Zotero.Schema = new function(){
yield Zotero.DB.queryAsync("UPDATE syncDeleteLog SET libraryID=1 WHERE libraryID=0"); yield Zotero.DB.queryAsync("UPDATE syncDeleteLog SET libraryID=1 WHERE libraryID=0");
yield Zotero.DB.queryAsync("ALTER TABLE syncDeleteLog RENAME TO syncDeleteLogOld"); yield Zotero.DB.queryAsync("ALTER TABLE syncDeleteLog RENAME TO syncDeleteLogOld");
yield Zotero.DB.queryAsync("CREATE TABLE syncDeleteLog (\n syncObjectTypeID INT NOT NULL,\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n synced INT NOT NULL DEFAULT 0,\n UNIQUE (syncObjectTypeID, libraryID, key),\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); yield Zotero.DB.queryAsync("CREATE TABLE syncDeleteLog (\n syncObjectTypeID INT NOT NULL,\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n UNIQUE (syncObjectTypeID, libraryID, key),\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncDeleteLog SELECT syncObjectTypeID, libraryID, key, timestamp, 0 FROM syncDeleteLogOld"); yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncDeleteLog SELECT syncObjectTypeID, libraryID, key, timestamp FROM syncDeleteLogOld");
yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS syncDeleteLog_timestamp"); yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS syncDeleteLog_timestamp");
yield Zotero.DB.queryAsync("CREATE INDEX syncDeleteLog_synced ON syncDeleteLog(synced)");
// TODO: Something special for tag deletions? // TODO: Something special for tag deletions?
//yield Zotero.DB.queryAsync("DELETE FROM syncDeleteLog WHERE syncObjectTypeID IN (2, 5, 6)"); //yield Zotero.DB.queryAsync("DELETE FROM syncDeleteLog WHERE syncObjectTypeID IN (2, 5, 6)");
//yield Zotero.DB.queryAsync("DELETE FROM syncObjectTypes WHERE syncObjectTypeID IN (2, 5, 6)"); //yield Zotero.DB.queryAsync("DELETE FROM syncObjectTypes WHERE syncObjectTypeID IN (2, 5, 6)");
yield Zotero.DB.queryAsync("UPDATE storageDeleteLog SET libraryID=1 WHERE libraryID=0"); yield Zotero.DB.queryAsync("UPDATE storageDeleteLog SET libraryID=1 WHERE libraryID=0");
yield Zotero.DB.queryAsync("ALTER TABLE storageDeleteLog RENAME TO storageDeleteLogOld"); yield Zotero.DB.queryAsync("ALTER TABLE storageDeleteLog RENAME TO storageDeleteLogOld");
yield Zotero.DB.queryAsync("CREATE TABLE storageDeleteLog (\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n synced INT NOT NULL DEFAULT 0,\n PRIMARY KEY (libraryID, key),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); yield Zotero.DB.queryAsync("CREATE TABLE storageDeleteLog (\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (libraryID, key),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO storageDeleteLog SELECT libraryID, key, timestamp, 0 FROM storageDeleteLogOld"); yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO storageDeleteLog SELECT libraryID, key, timestamp FROM storageDeleteLogOld");
yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS storageDeleteLog_timestamp"); yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS storageDeleteLog_timestamp");
yield Zotero.DB.queryAsync("CREATE INDEX storageDeleteLog_synced ON storageDeleteLog(synced)");
yield Zotero.DB.queryAsync("ALTER TABLE annotations RENAME TO annotationsOld"); yield Zotero.DB.queryAsync("ALTER TABLE annotations RENAME TO annotationsOld");
yield Zotero.DB.queryAsync("CREATE TABLE annotations (\n annotationID INTEGER PRIMARY KEY,\n itemID INT NOT NULL,\n parent TEXT,\n textNode INT,\n offset INT,\n x INT,\n y INT,\n cols INT,\n rows INT,\n text TEXT,\n collapsed BOOL,\n dateModified DATE,\n FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE\n)"); yield Zotero.DB.queryAsync("CREATE TABLE annotations (\n annotationID INTEGER PRIMARY KEY,\n itemID INT NOT NULL,\n parent TEXT,\n textNode INT,\n offset INT,\n x INT,\n y INT,\n cols INT,\n rows INT,\n text TEXT,\n collapsed BOOL,\n dateModified DATE,\n FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE\n)");

View File

@ -119,7 +119,7 @@ Zotero.Sync.APIClient.prototype = {
/** /**
* @return {Object|false} - An object with 'libraryVersion' and a 'deleted' array, or * @return {Object|false} - An object with 'libraryVersion' and a 'deleted' object, or
* false if 'since' is earlier than the beginning of the delete log * false if 'since' is earlier than the beginning of the delete log
*/ */
getDeleted: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, since) { getDeleted: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, since) {
@ -277,7 +277,7 @@ Zotero.Sync.APIClient.prototype = {
}, },
uploadObjects: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, objectType, method, version, objects) { uploadObjects: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, method, libraryVersion, objectType, objects) {
if (method != 'POST' && method != 'PATCH') { if (method != 'POST' && method != 'PATCH') {
throw new Error("Invalid method '" + method + "'"); throw new Error("Invalid method '" + method + "'");
} }
@ -287,7 +287,7 @@ Zotero.Sync.APIClient.prototype = {
Zotero.debug("Uploading " + objects.length + " " Zotero.debug("Uploading " + objects.length + " "
+ (objects.length == 1 ? objectType : objectTypePlural)); + (objects.length == 1 ? objectType : objectTypePlural));
Zotero.debug("Sending If-Unmodified-Since-Version: " + version); Zotero.debug("Sending If-Unmodified-Since-Version: " + libraryVersion);
var json = JSON.stringify(objects); var json = JSON.stringify(objects);
var params = { var params = {
@ -299,7 +299,7 @@ Zotero.Sync.APIClient.prototype = {
var xmlhttp = yield this.makeRequest(method, uri, { var xmlhttp = yield this.makeRequest(method, uri, {
headers: { headers: {
"If-Unmodified-Since-Version": version "If-Unmodified-Since-Version": libraryVersion
}, },
body: json, body: json,
successCodes: [200, 412] successCodes: [200, 412]
@ -309,17 +309,57 @@ Zotero.Sync.APIClient.prototype = {
Zotero.debug("Server returned 412: " + xmlhttp.responseText, 2); Zotero.debug("Server returned 412: " + xmlhttp.responseText, 2);
throw new Zotero.HTTP.UnexpectedStatusException(xmlhttp); throw new Zotero.HTTP.UnexpectedStatusException(xmlhttp);
} }
var libraryVersion = xmlhttp.getResponseHeader('Last-Modified-Version'); libraryVersion = xmlhttp.getResponseHeader('Last-Modified-Version');
if (!libraryVersion) { if (!libraryVersion) {
throw new Error("Last-Modified-Version not provided"); throw new Error("Last-Modified-Version not provided");
} }
return { return {
libraryVersion: libraryVersion, libraryVersion,
results: this._parseJSON(xmlhttp.responseText) results: this._parseJSON(xmlhttp.responseText)
}; };
}), }),
uploadDeletions: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, libraryVersion, objectType, keys) {
var objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
Zotero.debug(`Uploading ${keys.length} ${objectType} deletion`
+ (keys.length == 1 ? '' : 's'));
Zotero.debug("Sending If-Unmodified-Since-Version: " + libraryVersion);
var params = {
target: objectTypePlural,
libraryType: libraryType,
libraryTypeID: libraryTypeID
};
if (objectType == 'tag') {
params.tags = keys.join("||");
}
else {
params[objectType + "Key"] = keys.join(",");
}
var uri = this.buildRequestURI(params);
var xmlhttp = yield this.makeRequest("DELETE", uri, {
headers: {
"If-Unmodified-Since-Version": libraryVersion
},
successCodes: [204]
});
// Avoid logging error from Zotero.HTTP.request() in ConcurrentCaller
if (xmlhttp.status == 412) {
Zotero.debug("Server returned 412: " + xmlhttp.responseText, 2);
throw new Zotero.HTTP.UnexpectedStatusException(xmlhttp);
}
libraryVersion = xmlhttp.getResponseHeader('Last-Modified-Version');
if (!libraryVersion) {
throw new Error("Last-Modified-Version not provided");
}
return libraryVersion;
}),
buildRequestURI: function (params) { buildRequestURI: function (params) {
var uri = this.baseURL; var uri = this.baseURL;
@ -354,6 +394,7 @@ Zotero.Sync.APIClient.prototype = {
'itemKey', 'itemKey',
'collectionKey', 'collectionKey',
'searchKey', 'searchKey',
'tag',
'linkMode', 'linkMode',
'start', 'start',
'limit', 'limit',

View File

@ -67,6 +67,7 @@ Zotero.Sync.Data.Engine = function (options) {
this.stopOnError = options.stopOnError; this.stopOnError = options.stopOnError;
this.requests = []; this.requests = [];
this.uploadBatchSize = 25; this.uploadBatchSize = 25;
this.uploadDeletionBatchSize = 50;
this.failed = false; this.failed = false;
@ -567,35 +568,69 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
var uploadNeeded = false; var uploadNeeded = false;
var objectIDs = {}; var objectIDs = {};
var objectDeletions = {};
// Get unsynced local objects for each object type // Get unsynced local objects for each object type
for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(this.libraryID)) { for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(this.libraryID)) {
let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
let ids = yield Zotero.Sync.Data.Local.getUnsynced(this.libraryID, objectType); // New/modified objects
if (!ids.length) { let ids = yield Zotero.Sync.Data.Local.getUnsynced(objectType, this.libraryID);
Zotero.debug("No " + objectTypePlural + " to upload in " + this.libraryName); if (ids.length) {
continue;
}
Zotero.debug(ids.length + " " Zotero.debug(ids.length + " "
+ (ids.length == 1 ? objectType : objectTypePlural) + (ids.length == 1 ? objectType : objectTypePlural)
+ " to upload in library " + this.libraryID); + " to upload in library " + this.libraryID);
objectIDs[objectType] = ids; objectIDs[objectType] = ids;
}
else {
Zotero.debug("No " + objectTypePlural + " to upload in " + this.libraryName);
}
// Deleted objects
let keys = yield Zotero.Sync.Data.Local.getDeleted(objectType, this.libraryID);
if (keys.length) {
Zotero.debug(`${keys.length} ${objectType} deletion`
+ (keys.length == 1 ? '' : 's')
+ ` to upload in ${this.libraryName}`);
objectDeletions[objectType] = keys;
}
else {
Zotero.debug(`No ${objectType} deletions to upload in ${this.libraryName}`);
}
if (ids.length || keys.length) {
uploadNeeded = true; uploadNeeded = true;
} }
}
if (!uploadNeeded) { if (!uploadNeeded) {
return this.UPLOAD_RESULT_NOTHING_TO_UPLOAD; return this.UPLOAD_RESULT_NOTHING_TO_UPLOAD;
} }
Zotero.debug(JSON.stringify(objectIDs)); Zotero.debug(JSON.stringify(objectIDs));
for (let objectType in objectIDs) { for (let objectType in objectIDs) {
libraryVersion = yield this._uploadObjects(
objectType, objectIDs[objectType], libraryVersion
);
}
Zotero.debug(JSON.stringify(objectDeletions));
for (let objectType in objectDeletions) {
libraryVersion = yield this._uploadDeletions(
objectType, objectDeletions[objectType], libraryVersion
);
}
return this.UPLOAD_RESULT_SUCCESS;
});
Zotero.Sync.Data.Engine.prototype._uploadObjects = Zotero.Promise.coroutine(function* (objectType, ids, libraryVersion) {
let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
let queue = []; let queue = [];
for (let id of objectIDs[objectType]) { for (let id of ids) {
queue.push({ queue.push({
id: id, id: id,
json: null, json: null,
@ -609,10 +644,12 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
while (queue.length) { while (queue.length) {
// Get a slice of the queue and generate JSON for objects if necessary // Get a slice of the queue and generate JSON for objects if necessary
let batch = []; let batch = [];
let numSkipped = 0;
for (let i = 0; i < queue.length && queue.length < this.uploadBatchSize; i++) { for (let i = 0; i < queue.length && queue.length < this.uploadBatchSize; i++) {
let o = queue[i]; let o = queue[i];
// Skip requests that failed with 4xx // Skip requests that failed with 4xx
if (o.failed) { if (o.failed) {
numSkipped++;
continue; continue;
} }
if (!o.json) { if (!o.json) {
@ -627,7 +664,7 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
} }
// Remove selected and skipped objects from queue // Remove selected and skipped objects from queue
queue.splice(0, batch.length); queue.splice(0, batch.length + numSkipped);
Zotero.debug("UPLOAD BATCH:"); Zotero.debug("UPLOAD BATCH:");
Zotero.debug(batch); Zotero.debug(batch);
@ -637,9 +674,9 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
let json = yield this.apiClient.uploadObjects( let json = yield this.apiClient.uploadObjects(
this.libraryType, this.libraryType,
this.libraryTypeID, this.libraryTypeID,
objectType,
"POST", "POST",
libraryVersion, libraryVersion,
objectType,
batch batch
); );
@ -729,8 +766,6 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
} }
// Add failed objects back to end of queue // Add failed objects back to end of queue
Zotero.debug("ADDING BACK FAILED");
Zotero.debug(batch);
var numFailed = 0; var numFailed = 0;
for (let o of batch) { for (let o of batch) {
if (o !== undefined) { if (o !== undefined) {
@ -739,7 +774,6 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
numFailed++; numFailed++;
} }
} }
Zotero.debug(queue);
Zotero.debug("Failed: " + numFailed, 2); Zotero.debug("Failed: " + numFailed, 2);
} }
catch (e) { catch (e) {
@ -772,9 +806,73 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
} }
} }
Zotero.debug("Done uploading " + objectTypePlural + " in library " + this.libraryID); Zotero.debug("Done uploading " + objectTypePlural + " in library " + this.libraryID);
return libraryVersion;
})
Zotero.Sync.Data.Engine.prototype._uploadDeletions = Zotero.Promise.coroutine(function* (objectType, keys, libraryVersion) {
let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
let failureDelayGenerator = null;
while (keys.length) {
try {
let batch = keys.slice(0, this.uploadDeletionBatchSize);
libraryVersion = yield this.apiClient.uploadDeletions(
this.libraryType,
this.libraryTypeID,
libraryVersion,
objectType,
batch
);
keys.splice(0, batch.length);
// Update library version
this.library.libraryVersion = libraryVersion;
yield this.library.saveTx();
// Remove successful deletions from delete log
yield Zotero.Sync.Data.Local.removeObjectsFromDeleteLog(
objectType, this.libraryID, batch
);
}
catch (e) {
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
if (e.status == 412) {
return this.UPLOAD_RESULT_LIBRARY_CONFLICT;
} }
return this.UPLOAD_RESULT_SUCCESS; // On 5xx, delay and retry
if (e.status >= 500 && e.status <= 600) {
if (this.onError) {
this.onError(e);
}
if (this.stopOnError) {
throw new Error(e);
}
if (!failureDelayGenerator) {
// Keep trying for up to an hour
failureDelayGenerator = Zotero.Utilities.Internal.delayGenerator(
Zotero.Sync.Data.failureDelayIntervals, 60 * 60 * 1000
);
}
let keepGoing = yield failureDelayGenerator.next();
if (!keepGoing) {
Zotero.logError("Failed too many times");
throw e;
}
continue;
}
}
throw e;
}
}
Zotero.debug(`Done uploading ${objectType} deletions in ${this.libraryName}`);
return libraryVersion;
}); });
@ -1037,7 +1135,7 @@ Zotero.Sync.Data.Engine.prototype._fullSync = Zotero.Promise.coroutine(function*
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
let toDownload = []; let toDownload = [];
let cacheVersions = yield Zotero.Sync.Data.Local.getLatestCacheObjectVersions( let cacheVersions = yield Zotero.Sync.Data.Local.getLatestCacheObjectVersions(
this.libraryID, objectType objectType, this.libraryID
); );
// Queue objects that are out of date or don't exist locally // Queue objects that are out of date or don't exist locally
for (let key in results.versions) { for (let key in results.versions) {
@ -1082,7 +1180,7 @@ Zotero.Sync.Data.Engine.prototype._fullSync = Zotero.Promise.coroutine(function*
} }
// Mark synced objects that don't exist remotely as unsynced // Mark synced objects that don't exist remotely as unsynced
let syncedKeys = yield Zotero.Sync.Data.Local.getSynced(this.libraryID, objectType); let syncedKeys = yield Zotero.Sync.Data.Local.getSynced(objectType, this.libraryID);
let remoteMissing = Zotero.Utilities.arrayDiff(syncedKeys, Object.keys(results.versions)); let remoteMissing = Zotero.Utilities.arrayDiff(syncedKeys, Object.keys(results.versions));
if (remoteMissing.length) { if (remoteMissing.length) {
Zotero.debug("Checking remotely missing synced " + objectTypePlural); Zotero.debug("Checking remotely missing synced " + objectTypePlural);

View File

@ -37,9 +37,9 @@ Zotero.Sync.EventListeners.ChangeListener = new function () {
return; return;
} }
var syncSQL = "REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key, synced) " var syncSQL = "REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key) "
+ "VALUES (?, ?, ?, 0)"; + "VALUES (?, ?, ?)";
var storageSQL = "REPLACE INTO storageDeleteLog VALUES (?, ?, 0)"; var storageSQL = "REPLACE INTO storageDeleteLog (libraryID, key) VALUES (?, ?)";
var storageForLibrary = {}; var storageForLibrary = {};

View File

@ -134,10 +134,11 @@ Zotero.Sync.Data.Local = {
/** /**
* @param {String} objectType
* @param {Integer} libraryID * @param {Integer} libraryID
* @return {Promise<String[]>} - A promise for an array of object keys * @return {Promise<String[]>} - A promise for an array of object keys
*/ */
getSynced: function (libraryID, objectType) { getSynced: function (objectType, libraryID) {
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
var sql = "SELECT key FROM " + objectsClass.table + " WHERE libraryID=? AND synced=1"; var sql = "SELECT key FROM " + objectsClass.table + " WHERE libraryID=? AND synced=1";
return Zotero.DB.columnQueryAsync(sql, [libraryID]); return Zotero.DB.columnQueryAsync(sql, [libraryID]);
@ -145,10 +146,11 @@ Zotero.Sync.Data.Local = {
/** /**
* @param {String} objectType
* @param {Integer} libraryID * @param {Integer} libraryID
* @return {Promise<Integer[]>} - A promise for an array of object ids * @return {Promise<Integer[]>} - A promise for an array of object ids
*/ */
getUnsynced: Zotero.Promise.coroutine(function* (libraryID, objectType) { getUnsynced: Zotero.Promise.coroutine(function* (objectType, libraryID) {
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
var sql = "SELECT " + objectsClass.idColumn + " FROM " + objectsClass.table var sql = "SELECT " + objectsClass.idColumn + " FROM " + objectsClass.table
+ " WHERE libraryID=? AND synced=0"; + " WHERE libraryID=? AND synced=0";
@ -170,7 +172,7 @@ Zotero.Sync.Data.Local = {
* @return {Promise<Object>} - A promise for an object with object keys as keys and versions * @return {Promise<Object>} - A promise for an object with object keys as keys and versions
* as properties * as properties
*/ */
getLatestCacheObjectVersions: Zotero.Promise.coroutine(function* (libraryID, objectType) { getLatestCacheObjectVersions: Zotero.Promise.coroutine(function* (objectType, libraryID) {
var sql = "SELECT key, version FROM syncCache WHERE libraryID=? AND " var sql = "SELECT key, version FROM syncCache WHERE libraryID=? AND "
+ "syncObjectTypeID IN (SELECT syncObjectTypeID FROM " + "syncObjectTypeID IN (SELECT syncObjectTypeID FROM "
+ "syncObjectTypes WHERE name=?) ORDER BY version"; + "syncObjectTypes WHERE name=?) ORDER BY version";
@ -494,10 +496,10 @@ Zotero.Sync.Data.Local = {
// Auto-restore some locally deleted objects that have changed remotely // Auto-restore some locally deleted objects that have changed remotely
case 'collection': case 'collection':
case 'search': case 'search':
yield this._removeObjectFromDeleteLog( yield this.removeObjectsFromDeleteLog(
objectType, objectType,
libraryID, libraryID,
objectKey [objectKey]
); );
throw new Error("Unimplemented"); throw new Error("Unimplemented");
@ -1014,12 +1016,33 @@ Zotero.Sync.Data.Local = {
}), }),
/**
* @return {Promise<String[]>} - Promise for array of keys
*/
getDeleted: Zotero.Promise.coroutine(function* (objectType, libraryID) {
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
var sql = "SELECT key FROM syncDeleteLog WHERE libraryID=? AND syncObjectTypeID=?";
return Zotero.DB.columnQueryAsync(sql, [libraryID, syncObjectTypeID]);
}),
/** /**
* @return {Promise} * @return {Promise}
*/ */
_removeObjectFromDeleteLog: function (objectType, libraryID, key) { removeObjectsFromDeleteLog: function (objectType, libraryID, keys) {
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType); var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
var sql = "DELETE FROM syncDeleteLog WHERE libraryID=? AND key=? AND syncObjectTypeID=?"; var sql = "DELETE FROM syncDeleteLog WHERE libraryID=? AND syncObjectTypeID=? AND key IN (";
return Zotero.DB.queryAsync(sql, [libraryID, key, syncObjectTypeID]); return Zotero.DB.executeTransaction(function* () {
return Zotero.Utilities.Internal.forEachChunkAsync(
keys,
Zotero.DB.MAX_BOUND_PARAMETERS - 2,
Zotero.Promise.coroutine(function* (chunk) {
var params = [libraryID, syncObjectTypeID].concat(chunk);
return Zotero.DB.queryAsync(
sql + Array(chunk.length).fill('?').join(',') + ")", params
);
})
);
}.bind(this));
} }
} }

View File

@ -330,22 +330,18 @@ CREATE TABLE syncDeleteLog (
libraryID INT NOT NULL, libraryID INT NOT NULL,
key TEXT NOT NULL, key TEXT NOT NULL,
dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
synced INT NOT NULL DEFAULT 0,
UNIQUE (syncObjectTypeID, libraryID, key), UNIQUE (syncObjectTypeID, libraryID, key),
FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID), FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID),
FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE
); );
CREATE INDEX syncDeleteLog_synced ON syncDeleteLog(synced);
CREATE TABLE storageDeleteLog ( CREATE TABLE storageDeleteLog (
libraryID INT NOT NULL, libraryID INT NOT NULL,
key TEXT NOT NULL, key TEXT NOT NULL,
dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
synced INT NOT NULL DEFAULT 0,
PRIMARY KEY (libraryID, key), PRIMARY KEY (libraryID, key),
FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE
); );
CREATE INDEX storageDeleteLog_synced ON storageDeleteLog(synced);
CREATE TABLE annotations ( CREATE TABLE annotations (
annotationID INTEGER PRIMARY KEY, annotationID INTEGER PRIMARY KEY,

View File

@ -310,7 +310,7 @@ describe("Zotero.Sync.Data.Engine", function () {
assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion); assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion);
for (let type of types) { for (let type of types) {
// Make sure objects were set to the correct version and marked as synced // Make sure objects were set to the correct version and marked as synced
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(libraryID, type)), 0); assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(type, libraryID)), 0);
let key = objects[type][0].key; let key = objects[type][0].key;
let version = objects[type][0].version; let version = objects[type][0].version;
assert.equal(version, objectVersions[type][key]); assert.equal(version, objectVersions[type][key]);
@ -391,7 +391,7 @@ describe("Zotero.Sync.Data.Engine", function () {
assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion); assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion);
for (let type of types) { for (let type of types) {
// Make sure objects were set to the correct version and marked as synced // Make sure objects were set to the correct version and marked as synced
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(libraryID, type)), 0); assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(type, libraryID)), 0);
let o = objects[type][0]; let o = objects[type][0];
let key = o.key; let key = o.key;
let version = o.version; let version = o.version;
@ -475,7 +475,7 @@ describe("Zotero.Sync.Data.Engine", function () {
assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion); assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion);
for (let type of types) { for (let type of types) {
// Make sure local objects were updated with new metadata and marked as synced // Make sure local objects were updated with new metadata and marked as synced
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(libraryID, type)), 0); assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(type, libraryID)), 0);
let o = objects[type][0]; let o = objects[type][0];
let key = o.key; let key = o.key;
let version = o.version; let version = o.version;
@ -490,6 +490,66 @@ describe("Zotero.Sync.Data.Engine", function () {
} }
}) })
it("should upload local deletions", function* () {
var { engine, client, caller } = yield setup();
var libraryID = Zotero.Libraries.userLibraryID;
var lastLibraryVersion = 5;
yield Zotero.Libraries.setVersion(libraryID, lastLibraryVersion);
var types = Zotero.DataObjectUtilities.getTypes();
var objects = {};
for (let type of types) {
let obj1 = yield createDataObject(type);
let obj2 = yield createDataObject(type);
objects[type] = [obj1.key, obj2.key];
yield obj1.eraseTx();
yield obj2.eraseTx();
}
var count = types.length;
server.respond(function (req) {
if (req.method == "DELETE") {
assert.equal(
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
);
// TODO: Settings?
// Data objects
for (let type of types) {
let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
if (req.url.startsWith(baseURL + "users/1/" + typePlural)) {
let matches = req.url.match(new RegExp("\\?" + type + "Key=(.+)"));
let keys = decodeURIComponent(matches[1]).split(',');
assert.sameMembers(keys, objects[type]);
req.respond(
204,
{
"Last-Modified-Version": ++lastLibraryVersion
}
);
count--;
return;
}
}
}
})
yield engine.start();
assert.equal(count, 0);
for (let type of types) {
yield assert.eventually.lengthOf(
Zotero.Sync.Data.Local.getDeleted(type, libraryID), 0
);
}
assert.equal(
Zotero.Libraries.get(libraryID).libraryVersion,
lastLibraryVersion
);
})
it("should make only one request if in sync", function* () { it("should make only one request if in sync", function* () {
yield Zotero.Libraries.setVersion(Zotero.Libraries.userLibraryID, 5); yield Zotero.Libraries.setVersion(Zotero.Libraries.userLibraryID, 5);
({ engine, client, caller } = yield setup()); ({ engine, client, caller } = yield setup());
@ -911,10 +971,10 @@ describe("Zotero.Sync.Data.Engine", function () {
// Objects 1 should be marked as synced, with versions from the server // Objects 1 should be marked as synced, with versions from the server
// Objects 2 should be marked as unsynced // Objects 2 should be marked as unsynced
for (let type of types) { for (let type of types) {
var synced = yield Zotero.Sync.Data.Local.getSynced(userLibraryID, type); var synced = yield Zotero.Sync.Data.Local.getSynced(type, userLibraryID);
assert.deepEqual(synced, [objects[type][0].key]); assert.deepEqual(synced, [objects[type][0].key]);
assert.equal(objects[type][0].version, 10); assert.equal(objects[type][0].version, 10);
var unsynced = yield Zotero.Sync.Data.Local.getUnsynced(userLibraryID, type); var unsynced = yield Zotero.Sync.Data.Local.getUnsynced(type, userLibraryID);
assert.deepEqual(unsynced, [objects[type][1].id]); assert.deepEqual(unsynced, [objects[type][1].id]);
assert.equal(versionResults[type].libraryVersion, headers["Last-Modified-Version"]); assert.equal(versionResults[type].libraryVersion, headers["Last-Modified-Version"]);