
- New promise-based architecture - Library-specific file sync queues, allowing other libraries to continue if there's an error in one library - Library-specific sync errors, with error icons next to each library - Changed file uploading in on-demand download mode, which had been missing - On-demand download progress indicator in middle pane - More accurate progress indicator - Various tweaks and bug fixes - Various future tweaks and bug fixes
353 lines
9.2 KiB
JavaScript
353 lines
9.2 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2009 Center for History and New Media
|
|
George Mason University, Fairfax, Virginia, USA
|
|
http://zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
|
|
/**
|
|
* Transfer request for storage sync
|
|
*
|
|
* @param {String} name Identifier for request (e.g., "[libraryID]/[key]")
|
|
* @param {Function} onStart Callback to run when request starts
|
|
*/
|
|
Zotero.Sync.Storage.Request = function (name, callbacks) {
|
|
//Zotero.debug("Initializing request '" + name + "'");
|
|
|
|
this.callbacks = ['onStart', 'onProgress'];
|
|
|
|
this.name = name;
|
|
this.channel = null;
|
|
this.queue = null;
|
|
this.progress = 0;
|
|
this.progressMax = 0;
|
|
|
|
this._deferred = Q.defer();
|
|
this._running = false;
|
|
this._percentage = 0;
|
|
this._remaining = null;
|
|
this._maxSize = null;
|
|
this._finished = false;
|
|
this._changesMade = false;
|
|
|
|
for (var func in callbacks) {
|
|
if (this.callbacks.indexOf(func) !== -1) {
|
|
// Stuff all single functions into arrays
|
|
this['_' + func] = typeof callbacks[func] === 'function' ? [callbacks[func]] : callbacks[func];
|
|
}
|
|
else {
|
|
throw new Error("Invalid handler '" + func + "'");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.setMaxSize = function (size) {
|
|
this._maxSize = size;
|
|
};
|
|
|
|
|
|
/**
|
|
* Add callbacks from another request to this request
|
|
*/
|
|
Zotero.Sync.Storage.Request.prototype.importCallbacks = function (request) {
|
|
for each(var name in this.callbacks) {
|
|
name = '_' + name;
|
|
if (request[name]) {
|
|
// If no handlers for this event, add them all
|
|
if (!this[name]) {
|
|
this[name] = request[name];
|
|
continue;
|
|
}
|
|
// Otherwise add functions that don't already exist
|
|
var add = true;
|
|
for each(var newFunc in request[name]) {
|
|
for each(var currentFunc in this[name]) {
|
|
if (newFunc.toString() === currentFunc.toString()) {
|
|
Zotero.debug("Callback already exists in request -- not importing");
|
|
add = false;
|
|
break;
|
|
}
|
|
}
|
|
if (add) {
|
|
this[name].push(newFunc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.__defineGetter__('promise', function () {
|
|
return this._deferred.promise;
|
|
});
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.__defineGetter__('percentage', function () {
|
|
if (this._finished) {
|
|
return 100;
|
|
}
|
|
|
|
if (this.progressMax == 0) {
|
|
return 0;
|
|
}
|
|
|
|
var percentage = Math.round((this.progress / this.progressMax) * 100);
|
|
if (percentage < this._percentage) {
|
|
Zotero.debug(percentage + " is less than last percentage of "
|
|
+ this._percentage + " for request " + this.name, 2);
|
|
Zotero.debug(this.progress);
|
|
Zotero.debug(this.progressMax);
|
|
percentage = this._percentage;
|
|
}
|
|
else if (percentage > 100) {
|
|
Zotero.debug(percentage + " is greater than 100 for "
|
|
+ "request " + this.name, 2);
|
|
Zotero.debug(this.progress);
|
|
Zotero.debug(this.progressMax);
|
|
percentage = 100;
|
|
}
|
|
else {
|
|
this._percentage = percentage;
|
|
}
|
|
//Zotero.debug("Request '" + this.name + "' percentage is " + percentage);
|
|
return percentage;
|
|
});
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.__defineGetter__('remaining', function () {
|
|
if (this._finished) {
|
|
return 0;
|
|
}
|
|
|
|
if (!this.progressMax) {
|
|
if (this.queue.type == 'upload' && this._maxSize) {
|
|
return Math.round(Zotero.Sync.Storage.compressionTracker.ratio * this._maxSize);
|
|
}
|
|
|
|
//Zotero.debug("Remaining not yet available for request '" + this.name + "'");
|
|
return 0;
|
|
}
|
|
|
|
var remaining = this.progressMax - this.progress;
|
|
if (this._remaining === null) {
|
|
this._remaining = remaining;
|
|
}
|
|
else if (remaining > this._remaining) {
|
|
Zotero.debug(remaining + " is greater than the last remaining amount of "
|
|
+ this._remaining + " for request " + this.name);
|
|
remaining = this._remaining;
|
|
}
|
|
else if (remaining < 0) {
|
|
Zotero.debug(remaining + " is less than 0 for request " + this.name);
|
|
}
|
|
else {
|
|
this._remaining = remaining;
|
|
}
|
|
//Zotero.debug("Request '" + this.name + "' remaining is " + remaining);
|
|
return remaining;
|
|
});
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.setChannel = function (channel) {
|
|
this.channel = channel;
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.start = function () {
|
|
if (!this.queue) {
|
|
throw ("Request " + this.name + " must be added to a queue before starting");
|
|
}
|
|
|
|
Zotero.debug("Starting " + this.queue.name + " request " + this.name);
|
|
|
|
if (this._running) {
|
|
throw new Error("Request " + this.name + " already running");
|
|
}
|
|
|
|
this._running = true;
|
|
this.queue.activeRequests++;
|
|
|
|
if (this.queue.type == 'download') {
|
|
Zotero.Sync.Storage.setItemDownloadPercentage(this.name, 0);
|
|
}
|
|
|
|
var self = this;
|
|
|
|
// this._onStart is an array of promises returning changesMade.
|
|
//
|
|
// The main sync logic is triggered here.
|
|
|
|
Q.all([f(this) for each(f in this._onStart)])
|
|
.then(function (results) {
|
|
return {
|
|
localChanges: results.some(function (val) val && val.localChanges == true),
|
|
remoteChanges: results.some(function (val) val && val.remoteChanges == true),
|
|
conflict: results.reduce(function (prev, cur) {
|
|
return prev.conflict ? prev : cur;
|
|
}).conflict
|
|
};
|
|
})
|
|
.then(function (results) {
|
|
Zotero.debug('!!!!');
|
|
Zotero.debug(results);
|
|
|
|
if (results.localChanges) {
|
|
Zotero.debug("Changes were made by " + self.queue.name
|
|
+ " request " + self.name);
|
|
}
|
|
else {
|
|
Zotero.debug("No changes were made by " + self.queue.name
|
|
+ " request " + self.name);
|
|
}
|
|
|
|
// This promise updates localChanges/remoteChanges on the queue
|
|
self._deferred.resolve(results);
|
|
})
|
|
.fail(function (e) {
|
|
Zotero.debug(self.queue.Type + " request " + self.name + " failed");
|
|
Zotero.debug(self._deferred);
|
|
Zotero.debug(self._deferred.promise.isFulfilled());
|
|
self._deferred.reject(e);
|
|
Zotero.debug(self._deferred.promise.isFulfilled());
|
|
Zotero.debug(self._deferred.promise.isRejected());
|
|
})
|
|
// Finish the request (and in turn the queue, if this is the last request)
|
|
.fin(function () {
|
|
if (!self._finished) {
|
|
self._finish();
|
|
}
|
|
});
|
|
|
|
return this._deferred.promise;
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.isRunning = function () {
|
|
return this._running;
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.isFinished = function () {
|
|
return this._finished;
|
|
}
|
|
|
|
|
|
/**
|
|
* Update counters for given request
|
|
*
|
|
* Also updates progress meter
|
|
*
|
|
* @param {Integer} progress Progress so far
|
|
* (usually bytes transferred)
|
|
* @param {Integer} progressMax Max progress value for this request
|
|
* (usually total bytes)
|
|
*/
|
|
Zotero.Sync.Storage.Request.prototype.onProgress = function (channel, progress, progressMax) {
|
|
Zotero.debug(progress + "/" + progressMax + " for request " + this.name);
|
|
|
|
if (!this._running) {
|
|
Zotero.debug("Trying to update finished request " + this.name + " in "
|
|
+ "Zotero.Sync.Storage.Request.onProgress() "
|
|
+ "(" + progress + "/" + progressMax + ")", 2);
|
|
return;
|
|
}
|
|
|
|
if (!this.channel) {
|
|
this.channel = channel;
|
|
}
|
|
|
|
// Workaround for invalid progress values (possibly related to
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=451991 and fixed in 3.1)
|
|
if (progress < this.progress) {
|
|
Zotero.debug("Invalid progress for request '"
|
|
+ this.name + "' (" + progress + " < " + this.progress + ")");
|
|
return;
|
|
}
|
|
|
|
if (progressMax != this.progressMax) {
|
|
Zotero.debug("progressMax has changed from " + this.progressMax
|
|
+ " to " + progressMax + " for request '" + this.name + "'", 2);
|
|
}
|
|
|
|
this.progress = progress;
|
|
this.progressMax = progressMax;
|
|
this.queue.updateProgress();
|
|
|
|
if (this.queue.type == 'download') {
|
|
Zotero.Sync.Storage.setItemDownloadPercentage(this.name, this.percentage);
|
|
}
|
|
|
|
if (this.onProgress) {
|
|
for each(var f in this._onProgress) {
|
|
f(progress, progressMax);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Stop the request's underlying network request, if there is one
|
|
*/
|
|
Zotero.Sync.Storage.Request.prototype.stop = function () {
|
|
if (this.channel) {
|
|
try {
|
|
Zotero.debug("Stopping request '" + this.name + "'");
|
|
this.channel.cancel(0x804b0002); // NS_BINDING_ABORTED
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e);
|
|
}
|
|
}
|
|
else {
|
|
this._finish();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Mark request as finished and notify queue that it's done
|
|
*/
|
|
Zotero.Sync.Storage.Request.prototype._finish = function () {
|
|
Zotero.debug("Finishing " + this.queue.name + " request '" + this.name + "'");
|
|
this._finished = true;
|
|
var active = this._running;
|
|
this._running = false;
|
|
|
|
Zotero.Sync.Storage.setItemDownloadPercentage(this.name, false);
|
|
|
|
if (active) {
|
|
this.queue.activeRequests--;
|
|
}
|
|
// TEMP: mechanism for failures?
|
|
try {
|
|
this.queue.finishedRequests++;
|
|
this.queue.updateProgress();
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e);
|
|
Components.utils.reportError(e);
|
|
this._deferred.reject(e);
|
|
throw e;
|
|
}
|
|
}
|