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:
parent
6b8e5bafc6
commit
1e6c29766f
|
@ -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)");
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"]);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user