diff --git a/chrome/content/zotero-platform/mac/overlay.css b/chrome/content/zotero-platform/mac/overlay.css index e02cb1d70..495c12829 100644 --- a/chrome/content/zotero-platform/mac/overlay.css +++ b/chrome/content/zotero-platform/mac/overlay.css @@ -59,7 +59,7 @@ padding-top: 1px; } -#zotero-tb-sync-warning[error=true] +#zotero-tb-sync-error[error=true] { margin-bottom: 2px; } diff --git a/chrome/content/zotero-platform/win/overlay.css b/chrome/content/zotero-platform/win/overlay.css index 1313a941e..05dea2343 100644 --- a/chrome/content/zotero-platform/win/overlay.css +++ b/chrome/content/zotero-platform/win/overlay.css @@ -21,7 +21,7 @@ visibility: hidden; } -#zotero-tb-sync-warning { +#zotero-tb-sync-error { margin-right: 2px; } diff --git a/chrome/content/zotero/bindings/filesyncstatus.xml b/chrome/content/zotero/bindings/filesyncstatus.xml new file mode 100644 index 000000000..1ff75a9b3 --- /dev/null +++ b/chrome/content/zotero/bindings/filesyncstatus.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + [] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js index 04cbb2022..59cb81ae7 100644 --- a/chrome/content/zotero/xpcom/collectionTreeView.js +++ b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -240,7 +240,7 @@ Zotero.CollectionTreeView.prototype.reload = function() */ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids) { - if ((!ids || ids.length == 0) && action != 'refresh') { + if ((!ids || ids.length == 0) && action != 'refresh' && action != 'redraw') { return; } @@ -254,6 +254,11 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids) return; } + if (action == 'redraw') { + this._treebox.invalidate(); + return; + } + this.selection.selectEventsSuppressed = true; var savedSelection = this.saveSelection(); @@ -420,8 +425,12 @@ Zotero.CollectionTreeView.prototype.getCellText = function(row, column) { var obj = this._getItemAtRow(row); - if(column.id == "zotero-collections-name-column") + if (column.id == 'zotero-collections-name-column') { return obj.getName(); + } + else if (column.id == 'zotero-collections-sync-status-column') { + return ""; + } else return ""; } @@ -430,7 +439,41 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col) { var itemGroup = this._getItemAtRow(row); var collectionType = itemGroup.type; + + if (collectionType == 'group') { + collectionType = 'library'; + } + + // Show sync icons only in library rows + if (collectionType != 'library' && col.index != 0) { + return ''; + } + switch (collectionType) { + case 'library': + if (col.id == 'zotero-collections-sync-status-column') { + if (itemGroup.isLibrary(true)) { + var libraryID = itemGroup.isLibrary() ? 0 : itemGroup.ref.libraryID; + var errors = Zotero.Sync.Runner.getErrors(libraryID); + if (errors) { + var e = Zotero.Sync.Runner.getPrimaryError(errors); + switch (e.status) { + case 'warning': + var image = 'error'; + break; + + default: + var image = 'exclamation'; + break; + } + + return 'chrome://zotero/skin/' + image + '.png'; + } + } + return ''; + } + break; + case 'trash': if (this._trashNotEmpty[itemGroup.ref.libraryID ? itemGroup.ref.libraryID : 0]) { collectionType += '-full'; @@ -446,7 +489,7 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col) } break; - case 'group': + collectionType = 'library'; break; diff --git a/chrome/content/zotero/xpcom/data/dataObjects.js b/chrome/content/zotero/xpcom/data/dataObjects.js index dbc71d302..72cd72cd2 100644 --- a/chrome/content/zotero/xpcom/data/dataObjects.js +++ b/chrome/content/zotero/xpcom/data/dataObjects.js @@ -112,7 +112,7 @@ Zotero.DataObjects = function (object, objectPlural, id, table) { } else { sql += "libraryID"; - if (libraryID) { + if (libraryID && libraryID !== '0') { sql += "=? "; params.push(libraryID); } diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js index adf9f27aa..8b868462e 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -674,7 +674,7 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) { this._disabledCheck(); - //Zotero.debug("Setting field '" + field + "' to '" + value + "' (loadIn: " + (loadIn ? 'true' : 'false') + ")"); + //Zotero.debug("Setting field '" + field + "' to '" + value + "' (loadIn: " + (loadIn ? 'true' : 'false') + ") for item " + this.id + " "); if (!field) { throw ("Field not specified in Item.setField()"); @@ -1609,6 +1609,7 @@ Zotero.Item.prototype.save = function() { 'libraryID', 'key' ]; + for each(var field in updateFields) { if (this._changedPrimaryData && this._changedPrimaryData[field]) { sql += field + '=?, '; @@ -3000,7 +3001,6 @@ Zotero.Item.prototype.__defineSetter__('attachmentLinkMode', function (val) { if (val === this.attachmentLinkMode) { return; } - if (!this._changedAttachmentData) { this._changedAttachmentData = {}; } diff --git a/chrome/content/zotero/xpcom/data/libraries.js b/chrome/content/zotero/xpcom/data/libraries.js index 81e13911d..e631f85bd 100644 --- a/chrome/content/zotero/xpcom/data/libraries.js +++ b/chrome/content/zotero/xpcom/data/libraries.js @@ -45,6 +45,10 @@ Zotero.Libraries = new function () { this.getName = function (libraryID) { + if (!libraryID) { + return Zotero.getString('pane.collections.library'); + } + var type = this.getType(libraryID); switch (type) { case 'group': @@ -59,6 +63,9 @@ Zotero.Libraries = new function () { this.getType = function (libraryID) { + if (libraryID === 0) { + return 'user'; + } var sql = "SELECT libraryType FROM libraries WHERE libraryID=?"; var libraryType = Zotero.DB.valueQuery(sql, libraryID); if (!libraryType) { diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js index b70a16c29..4d13d7727 100644 --- a/chrome/content/zotero/xpcom/db.js +++ b/chrome/content/zotero/xpcom/db.js @@ -402,7 +402,8 @@ Zotero.DBConnection.prototype.getStatement = function (sql, params, checkParams) } else { if (checkParams && numParams > 0) { - throw ("No parameters provided for query containing placeholders"); + throw ("No parameters provided for query containing placeholders " + + "[QUERY: " + sql + "]"); } } return statement; diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js index 1d257d806..99687ad83 100644 --- a/chrome/content/zotero/xpcom/itemTreeView.js +++ b/chrome/content/zotero/xpcom/itemTreeView.js @@ -344,9 +344,29 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) var savedSelection = this.saveSelection(); var previousRow = false; - // Redraw the tree (for tag color changes) + // Redraw the tree (for tag color and progress changes) if (action == 'redraw') { - this._treebox.invalidate(); + // Redraw specific rows + if (type == 'item' && ids.length) { + // Redraw specific cells + if (extraData && extraData.column) { + var col = this._treebox.columns.getNamedColumn( + 'zotero-items-column-' + extraData.column + ); + for each(var id in ids) { + this._treebox.invalidateCell(this._itemRowMap[id], col); + } + } + else { + for each(var id in ids) { + this._treebox.invalidateRow(this._itemRowMap[id]); + } + } + } + // Redraw the whole tree + else { + this._treebox.invalidate(); + } return; } @@ -849,6 +869,12 @@ Zotero.ItemTreeView.prototype.getImageSrc = function(row, col) if (this._itemGroup.isTrash()) return false; var treerow = this._getItemAtRow(row); + + if ((!this.isContainer(row) || !this.isContainerOpen(row)) + && Zotero.Sync.Storage.getItemDownloadImageNumber(treerow.ref)) { + return ''; + } + if (treerow.level === 0) { if (treerow.ref.isRegularItem()) { switch (treerow.ref.getBestAttachmentState()) { @@ -2746,7 +2772,8 @@ Zotero.ItemTreeView.prototype.getRowProperties = function(row, prop) { } Zotero.ItemTreeView.prototype.getColumnProperties = function(col, prop) { } Zotero.ItemTreeView.prototype.getCellProperties = function(row, col, prop) { - var itemID = this._getItemAtRow(row).ref.id; + var treeRow = this._getItemAtRow(row); + var itemID = treeRow.ref.id; // Set tag colors // @@ -2767,6 +2794,30 @@ Zotero.ItemTreeView.prototype.getCellProperties = function(row, col, prop) { getService(Components.interfaces.nsIAtomService); prop.AppendElement(aServ.getAtom("contextRow")); } + + // Mark hasAttachment column, which needs special image handling + if (col.id == 'zotero-items-column-hasAttachment') { + var aServ = Components.classes["@mozilla.org/atom-service;1"]. + getService(Components.interfaces.nsIAtomService); + prop.AppendElement(aServ.getAtom("hasAttachment")); + + // Don't show pie for open parent items, since we show it for the + // child item + if (this.isContainer(row) && this.isContainerOpen(row)) { + return; + } + + var num = Zotero.Sync.Storage.getItemDownloadImageNumber(treeRow.ref); + //var num = Math.round(new Date().getTime() % 10000 / 10000 * 64); + if (num !== false) { + if (!aServ) { + var aServ = Components.classes["@mozilla.org/atom-service;1"]. + getService(Components.interfaces.nsIAtomService); + } + prop.AppendElement(aServ.getAtom("pie")); + prop.AppendElement(aServ.getAtom("pie" + num)); + } + } } Zotero.ItemTreeView.TreeRow = function(ref, level, isOpen) diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js index e57234e21..f4042aba2 100644 --- a/chrome/content/zotero/xpcom/storage.js +++ b/chrome/content/zotero/xpcom/storage.js @@ -36,6 +36,7 @@ Zotero.Sync.Storage = new function () { this.SUCCESS = 1; this.ERROR_NO_URL = -1; + this.ERROR_NO_USERNAME = -2; this.ERROR_NO_PASSWORD = -3; this.ERROR_OFFLINE = -4; this.ERROR_UNREACHABLE = -5; @@ -68,10 +69,9 @@ Zotero.Sync.Storage = new function () { compressed: 0, uncompressed: 0, get ratio() { - return Math.round( - (Zotero.Sync.Storage.compressionTracker.uncompressed - + return (Zotero.Sync.Storage.compressionTracker.uncompressed - Zotero.Sync.Storage.compressionTracker.compressed) / - Zotero.Sync.Storage.compressionTracker.uncompressed * 100); + Zotero.Sync.Storage.compressionTracker.uncompressed; } } @@ -80,134 +80,302 @@ Zotero.Sync.Storage = new function () { // var _syncInProgress; var _updatesInProgress; - var _changesMade; - var _resyncOnFinish; + var _itemDownloadPercentages = {}; + + + this.sync = function (libraries) { + if (libraries) { + Zotero.debug("Starting file sync for libraries " + libraries); + } + else { + Zotero.debug("Starting file sync"); + } + + var self = this; + + var libraryModes = {}; + var librarySyncTimes = {}; + + // Get personal library file sync mode + return Q.fcall(function () { + // TODO: Make sure modes are active + + if (libraries && libraries.indexOf(0) == -1) { + return; + } + + if (Zotero.Sync.Storage.ZFS.includeUserFiles) { + libraryModes[0] = Zotero.Sync.Storage.ZFS; + return; + } + + if (Zotero.Sync.Storage.WebDAV.includeUserFiles) { + if (!Zotero.Sync.Storage.WebDAV.verified) { + Zotero.debug("WebDAV file sync is not active"); + + // Try to verify server now if it hasn't been + return mode.checkServerPromise() + .then(function () { + libraryModes[0] = Zotero.Sync.Storage.WebDAV; + }); + } + + libraryModes[0] = Zotero.Sync.Storage.WebDAV; + return; + } + }) + .then(function () { + // Get group library file sync modes + if (Zotero.Sync.Storage.ZFS.includeGroupFiles) { + var groups = Zotero.Groups.getAll(); + for each(var group in groups) { + if (libraries && libraries.indexOf(group.libraryID) == -1) { + continue; + } + // TODO: if library file syncing enabled + libraryModes[group.libraryID] = Zotero.Sync.Storage.ZFS; + } + } + + // Cache auth credentials for each mode + var modes = []; + var promises = []; + for each(var mode in libraryModes) { + if (modes.indexOf(mode) == -1) { + modes.push(mode); + promises.push(mode.cacheCredentials()); + } + } + return Q.allResolved(promises) + .then(function () { + var promises = []; + for (var libraryID in libraryModes) { + libraryID = parseInt(libraryID); + // Get the last sync time for each library + if (self.downloadOnSync(libraryID)) { + promises.push(Q.allResolved( + [libraryID, libraryModes[libraryID].getLastSyncTime(libraryID)] + )); + } + // If download-as-needed, we don't need the last sync time + else { + promises.push(Q.allResolved( + [libraryID, null] + )); + } + } + // 'promises' is an array of promises for arrays containing promises + // for a libraryID and the last sync time for that library + return Q.allResolved(promises); + }); + }) + .then(function (promises) { + if (!promises.length) { + Zotero.debug("No libraries are active for file sync"); + return []; + } + + promises.forEach(function (p) { + p = p.valueOf(); + var libraryID = p[0].valueOf(); + Zotero.debug(libraryID); + if (p[1].isFulfilled()) { + librarySyncTimes[libraryID] = p[1].valueOf(); + } + else { + // TODO: error log of some sort + //librarySyncTimes[libraryID] = p[1].valueOf().exception; + Components.utils.reportError(p[1].valueOf().exception); + } + }); + + // Queue files to download and upload from each library + for (var libraryID in librarySyncTimes) { + var lastSyncTime = librarySyncTimes[libraryID]; + libraryID = parseInt(libraryID); + + self.checkForUpdatedFiles(null, libraryID); + + var downloadAll = self.downloadOnSync(libraryID); + + // Forced downloads happen even in on-demand mode + var sql = "SELECT COUNT(*) FROM items " + + "JOIN itemAttachments USING (itemID) " + + "WHERE libraryID=? AND syncState=?"; + var downloadForced = !!Zotero.DB.valueQuery( + sql, + [ + libraryID == 0 ? null : libraryID, + Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD + ] + ); + + // If we don't have any forced downloads, we can skip + // downloads if the last sync time hasn't changed + if (downloadAll && !downloadForced && lastSyncTime) { + var version = self.getStoredLastSyncTime( + libraryModes[libraryID], libraryID + ); + if (version == lastSyncTime) { + Zotero.debug("Last " + libraryModes[libraryID].name + + " sync time hasn't changed for library " + + libraryID + " -- skipping file downloads"); + downloadAll = false; + } + } + + if (downloadAll || downloadForced) { + Zotero.debug(downloadAll); + for each(var itemID in _getFilesToDownload(libraryID, !downloadAll)) { + var item = Zotero.Items.get(itemID); + self.queueItem(item); + } + } + + // Get files to upload + for each(var itemID in _getFilesToUpload(libraryID)) { + var item = Zotero.Items.get(itemID); + self.queueItem(item); + } + } + + // TODO: change to start() for each library, with allResolved() + // for the whole set and all() for each library + return Zotero.Sync.Storage.QueueManager.start(); + }) + .then(function (promises) { + Zotero.debug('Queue manager is finished'); + + var changedLibraries = []; + + var finalPromises = []; + + promises.forEach(function (promise) { + var result = promise.valueOf(); + if (promise.isFulfilled()) { + Zotero.debug("File " + result.type + " sync finished " + + "for library " + result.libraryID); + Zotero.debug(result); + if (result.localChanges) { + changedLibraries.push(result.libraryID); + } + finalPromises.push(Q.allResolved([ + result.libraryID, + libraryModes[result.libraryID].setLastSyncTime( + result.libraryID, + result.remoteChanges + ? false : librarySyncTimes[result.libraryID] + ) + ])); + } + else { + result = result.exception; + Zotero.debug("File " + result.type + " sync failed " + + "for library " + result.libraryID); + + finalPromises.push([result.libraryID, promise]); + } + }); + + if (promises.length && !changedLibraries.length) { + Zotero.debug("No local changes made during file sync"); + } + + return Q.allResolved(finalPromises) + .then(function (promises) { + var results = { + changesMade: !!changedLibraries.length, + errors: [] + }; + + promises.forEach(function (p) { + // If this is a promise, get an array + if (Q.isPromise(p)) { + p = p.valueOf(); + } + var libraryID = p[0].valueOf(); + if (p[1].isRejected()) { + var result = p[1].valueOf(); + result = result.exception; + if (typeof result == 'string') { + result = new Error(result); + } + result.libraryID = libraryID; + results.errors.push(result); + } + }); + + return results; + }); + }); + } // // Public methods // - this.sync = function (modeName, observer) { - var mode = getModeFromName(modeName); - - if (!observer) { - throw new Error("Observer not provided"); + this.queueItem = function (item, highPriority) { + if (item.libraryID) { + var library = item.libraryID; + var mode = Zotero.Sync.Storage.ZFS; } - registerDefaultObserver(modeName); - Zotero.Sync.Storage.EventManager.registerObserver(observer, true, modeName); - - if (!mode.active) { - if (!mode.enabled) { - Zotero.debug(mode.name + " file sync is not enabled"); - Zotero.Sync.Storage.EventManager.skip(); - return; - } - - Zotero.debug(mode.name + " file sync is not active"); - - // Try to verify server now if it hasn't been - if (!mode.verified) { - mode.checkServer(function (uri, status) { - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - var lastWin = wm.getMostRecentWindow("navigator:browser"); - - var success = mode.checkServerCallback(uri, status, lastWin, true); - if (success) { - Zotero.debug(mode.name + " file sync is successfully set up"); - Zotero.Sync.Storage.sync(mode.name); + else { + var library = 0; + var mode = Zotero.Sync.Storage.ZFS.includeUserFiles + ? Zotero.Sync.Storage.ZFS : Zotero.Sync.Storage.WebDAV; + } + switch (Zotero.Sync.Storage.getSyncState(item.id)) { + case this.SYNC_STATE_TO_DOWNLOAD: + case this.SYNC_STATE_FORCE_DOWNLOAD: + var queue = Zotero.Sync.Storage.QueueManager.get('download', library); + var callbacks = { + onStart: function (request) { + return mode.downloadFile(request); } - else { - Zotero.debug(mode.name + " verification failed"); - - var e = new Zotero.Error( - Zotero.getString('sync.storage.error.verificationFailed', mode.name), - 0, - { - dialogButtonText: Zotero.getString('sync.openSyncPreferences'), - dialogButtonCallback: function () { - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - var lastWin = wm.getMostRecentWindow("navigator:browser"); - lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync'); - } - } - ); - Zotero.Sync.Storage.EventManager.error(e, true); + }; + break; + + case this.SYNC_STATE_TO_UPLOAD: + case this.SYNC_STATE_FORCE_UPLOAD: + var queue = Zotero.Sync.Storage.QueueManager.get('upload', library); + var callbacks = { + onStart: function (request) { + return mode.uploadFile(request); } - }); - } + }; + break; - return; - } - - if (!mode.includeUserFiles && !mode.includeGroupFiles) { - Zotero.debug("No libraries are enabled for " + mode.name + " syncing"); - Zotero.Sync.Storage.EventManager.skip(); - return; - } - - if (_syncInProgress) { - Zotero.Sync.Storage.EventManager.error( - "File sync operation already in progress" - ); - } - - Zotero.debug("Beginning " + mode.name + " file sync"); - _syncInProgress = true; - _changesMade = false; - - try { - Zotero.Sync.Storage.checkForUpdatedFiles( - null, - null, - mode.includeUserFiles && Zotero.Sync.Storage.downloadOnSync(), - mode.includeGroupFiles && Zotero.Sync.Storage.downloadOnSync('groups') - ); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } - - var self = this; - - mode.getLastSyncTime(function (lastSyncTime) { - // Register the observers again to make sure they're active when we - // start the queues. (They'll only be registered once.) Observers are - // cleared when all queues finish, so without this another sync - // process (e.g., on-demand download) could finish and clear all - // observers while getLastSyncTime() is running. - registerDefaultObserver(modeName); - Zotero.Sync.Storage.EventManager.registerObserver(observer, true, modeName); - - var download = true; - - var sql = "SELECT COUNT(*) FROM itemAttachments WHERE syncState=?"; - var force = !!Zotero.DB.valueQuery(sql, Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD); - - if (!force && lastSyncTime) { - var sql = "SELECT version FROM version WHERE schema='storage_" + modeName + "'"; - var version = Zotero.DB.valueQuery(sql); - if (version == lastSyncTime) { - Zotero.debug("Last " + mode.name + " sync time hasn't changed -- skipping file download step"); - download = false; - } - } - - try { - var activeDown = download ? _downloadFiles(mode) : false; - var activeUp = _uploadFiles(mode); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } - - if (!activeDown && !activeUp) { - Zotero.Sync.Storage.EventManager.skip(); + case false: + Zotero.debug("Sync state for item " + item.id + " not found", 2); return; - } - }); - } + } + + var request = new Zotero.Sync.Storage.Request( + (item.libraryID ? item.libraryID : 0) + '/' + item.key, callbacks + ); + request.setMaxSize(Zotero.Attachments.getTotalFileSize(item)); + queue.addRequest(request, highPriority); + }; + + + this.getStoredLastSyncTime = function (mode, libraryID) { + var sql = "SELECT version FROM version WHERE schema=?"; + return Zotero.DB.valueQuery( + sql, "storage_" + mode.name.toLowerCase() + "_" + libraryID + ); + }; + + + this.setStoredLastSyncTime = function (mode, libraryID, time) { + var sql = "REPLACE INTO version SET version=? WHERE schema=?"; + Zotero.DB.query( + sql, + [ + time, + "storage_" + mode.name.toLowerCase() + "_" + libraryID + ] + ); + }; /** @@ -233,10 +401,8 @@ Zotero.Sync.Storage = new function () { break; default: - Zotero.Sync.Storage.EventManager.error( - "Invalid sync state '" + syncState - + "' in Zotero.Sync.Storage.setSyncState()" - ); + throw "Invalid sync state '" + syncState + + "' in Zotero.Sync.Storage.setSyncState()" } var sql = "UPDATE itemAttachments SET syncState=? WHERE itemID=?"; @@ -253,9 +419,8 @@ Zotero.Sync.Storage = new function () { var sql = "SELECT storageModTime FROM itemAttachments WHERE itemID=?"; var mtime = Zotero.DB.valueQuery(sql, itemID); if (mtime === false) { - Zotero.Sync.Storage.EventManager.error( - "Item " + itemID + " not found in Zotero.Sync.Storage.getSyncedModificationTime()" - ); + throw "Item " + itemID + " not found in " + + "Zotero.Sync.Storage.getSyncedModificationTime()"; } return mtime; } @@ -298,9 +463,8 @@ Zotero.Sync.Storage = new function () { var sql = "SELECT storageHash FROM itemAttachments WHERE itemID=?"; var hash = Zotero.DB.valueQuery(sql, itemID); if (hash === false) { - Zotero.Sync.Storage.EventManager.error( - "Item " + itemID + " not found in Zotero.Sync.Storage.getSyncedHash()" - ); + throw "Item " + itemID + " not found in " + + "Zotero.Sync.Storage.getSyncedHash()"; } return hash; } @@ -374,7 +538,7 @@ Zotero.Sync.Storage = new function () { */ this.downloadAsNeeded = function (libraryID) { // Personal library - if (libraryID == null) { + if (!libraryID) { return Zotero.Prefs.get('sync.storage.downloadMode.personal') == 'on-demand'; } // Group library (groupID or 'groups') @@ -389,7 +553,7 @@ Zotero.Sync.Storage = new function () { */ this.downloadOnSync = function (libraryID) { // Personal library - if (libraryID == null) { + if (!libraryID) { return Zotero.Prefs.get('sync.storage.downloadMode.personal') == 'on-sync'; } // Group library (groupID or 'groups') @@ -401,14 +565,10 @@ Zotero.Sync.Storage = new function () { /** - * Scans local files and marks any that have changed as 0 for uploading - * and any that are missing as 1 for downloading + * Scans local files and marks any that have changed for uploading + * and any that are missing for downloading * - * Also marks missing files for downloading - * - * @param {Integer[]} [itemIDs] An optional set of item ids to check - * @param {Object} [itemModTimes] Item mod times indexed by item ids - * appearing in itemIDs; if set, + * @param {Object} [itemModTimes] Item mod times indexed by item ids; * items with stored mod times * that differ from the provided * time but file mod times @@ -419,30 +579,25 @@ Zotero.Sync.Storage = new function () { * @return {Boolean} TRUE if any items changed state, * FALSE otherwise */ - this.checkForUpdatedFiles = function (itemIDs, itemModTimes, includeUserFiles, includeGroupFiles) { - Zotero.debug("Checking for locally changed attachment files"); - // check for current ops? + this.checkForUpdatedFiles = function (itemModTimes, libraryID) { + var msg = "Checking for locally changed attachment files"; - if (itemIDs) { - if (includeUserFiles || includeGroupFiles) { - throw new Error("includeUserFiles and includeGroupFiles are not allowed when itemIDs"); + if (typeof libraryID != 'undefined') { + msg += " in library " + libraryID; + if (itemModTimes) { + throw new Error("libraryID is not allowed when itemIDs is set"); } } else { - if (!includeUserFiles && !includeGroupFiles) { + if (!itemModTimes) { return false; } } - - if (itemModTimes && !itemIDs) { - throw new Error("itemModTimes can only be set if itemIDs is an array"); - } + Zotero.debug(msg); var changed = false; - if (!itemIDs) { - itemIDs = []; - } + var itemIDs = Object.keys(itemModTimes ? itemModTimes : {}); // Can only handle 999 bound parameters at a time var numIDs = itemIDs.length; @@ -457,18 +612,17 @@ Zotero.Sync.Storage = new function () { var sql = "SELECT itemID, linkMode, path, storageModTime, storageHash, syncState " + "FROM itemAttachments JOIN items USING (itemID) " + "WHERE linkMode IN (?,?) AND syncState IN (?,?)"; - if (includeUserFiles && !includeGroupFiles) { - sql += " AND libraryID IS NULL"; - } - else if (!includeUserFiles && includeGroupFiles) { - sql += " AND libraryID IS NOT NULL"; - } - var params = [ + var params = []; + params.push( Zotero.Attachments.LINK_MODE_IMPORTED_FILE, Zotero.Attachments.LINK_MODE_IMPORTED_URL, Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC - ]; + ); + if (typeof libraryID != 'undefined') { + sql += " AND libraryID=?"; + params.push(libraryID == 0 ? null : libraryID); + } if (chunk.length) { sql += " AND itemID IN (" + chunk.map(function () '?').join() + ")"; params = params.concat(chunk); @@ -481,13 +635,19 @@ Zotero.Sync.Storage = new function () { } while (done < numIDs); - if (!rows) { - Zotero.debug("No to-upload or in-sync files found"); + // If no files, or everything is already marked for download, + // we don't need to do anything + if (!rows.length) { + var msg = "No in-sync or to-upload files found"; + if (typeof libraryID != 'undefined') { + msg += " in library " + libraryID; + } + Zotero.debug(msg); Zotero.DB.commitTransaction(); return changed; } - // Index data by item id + // Index attachment data by item id var itemIDs = []; var attachmentData = {}; for each(var row in rows) { @@ -501,48 +661,51 @@ Zotero.Sync.Storage = new function () { state: row.syncState }; } - if (itemIDs.length == 0) { - Zotero.DB.commitTransaction(); - return changed; - } - - rows = undefined; + rows = null; var updatedStates = {}; var items = Zotero.Items.get(itemIDs); for each(var item in items) { + var lk = libraryID + "/" + item.key; + Zotero.debug("Checking attachment file for item " + lk); var file = item.getFile(attachmentData[item.id]); if (!file) { - Zotero.debug("Marking attachment " + item.id + " as missing"); + Zotero.debug("Marking attachment " + lk + " as missing"); updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD; continue; } + // If file is already marked for upload, skip check. Even if this + // is download-marking mode (itemModTimes) and the file was + // changed remotely, conflicts are checked at upload time, so we + // don't need to worry about it here. + if (attachmentData[item.id].state == Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD) { + continue; + } + var fmtime = item.attachmentModificationTime; - //Zotero.debug("Stored mtime is " + attachmentData[item.id].mtime); - //Zotero.debug("File mtime is " + fmtime); + Zotero.debug("Stored mtime is " + attachmentData[item.id].mtime); + Zotero.debug("File mtime is " + fmtime); // Download-marking mode if (itemModTimes) { - Zotero.debug("Item mod time is " + itemModTimes[item.id]); + Zotero.debug("Remote mod time for item " + lk + " is " + itemModTimes[item.id]); - // Ignore attachments whose storage mod times haven't changed + // Ignore attachments whose stored mod times haven't changed if (row.storageModTime == itemModTimes[id]) { Zotero.debug("Storage mod time (" + row.storageModTime + ") " - + "hasn't changed for attachment " + id); + + "hasn't changed for item " + lk); continue; } - Zotero.debug("Marking attachment " + item.id + " for download"); + Zotero.debug("Marking attachment " + lk + " for download"); updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD; - - continue; } var mtime = attachmentData[item.id].mtime; - // If stored time matches file, it hasn't changed + // If stored time matches file, it hasn't changed locally if (mtime == fmtime) { continue; } @@ -550,8 +713,9 @@ Zotero.Sync.Storage = new function () { // Allow floored timestamps for filesystems that don't support // millisecond precision (e.g., HFS+) if (Math.floor(mtime / 1000) * 1000 == fmtime || Math.floor(fmtime / 1000) * 1000 == mtime) { - Zotero.debug("File mod times are within one-second precision (" + fmtime + " ≅ " + mtime + ") " - + "for " + file.leafName + " -- ignoring"); + Zotero.debug("File mod times are within one-second precision " + + "(" + fmtime + " ≅ " + mtime + ") for " + file.leafName + + " for item " + lk + " -- ignoring"); continue; } @@ -561,28 +725,25 @@ Zotero.Sync.Storage = new function () { // And check with one-second precision as well || Math.abs(fmtime - Math.floor(mtime / 1000) * 1000) == 3600000 || Math.abs(Math.floor(fmtime / 1000) * 1000 - mtime) == 3600000) { - Zotero.debug("File mod time (" + fmtime + ") is exactly one hour off remote file (" + mtime + ") " + Zotero.debug("File mod time (" + fmtime + ") is exactly one " + + "hour off remote file (" + mtime + ") for item " + lk + "-- assuming time zone issue and skipping upload"); continue; } - // If file is already marked for upload, skip - if (attachmentData[item.id].state == Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD) { - continue; - } - // If file hash matches stored hash, only the mod time changed, so skip var f = item.getFile(); if (f) { Zotero.debug(f.path); } else { - Zotero.debug("File missing before getting hash"); + Zotero.debug("File for item " + lk + " missing before getting hash"); } var fileHash = item.attachmentHash; if (attachmentData[item.id].hash && attachmentData[item.id].hash == fileHash) { Zotero.debug("Mod time didn't match (" + fmtime + "!=" + mtime + ") " - + "but hash did for " + file.leafName + " -- updating file mod time"); + + "but hash did for " + file.leafName + " for item " + lk + + " -- updating file mod time"); try { file.lastModifiedTime = attachmentData[item.id].mtime; } @@ -592,8 +753,9 @@ Zotero.Sync.Storage = new function () { continue; } - Zotero.debug("Marking attachment " + item.id + " as changed (" - + mtime + " != " + fmtime + ")"); + // Mark file for upload + Zotero.debug("Marking attachment " + lk + " as changed " + + "(" + mtime + " != " + fmtime + ")"); updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD; } @@ -606,8 +768,6 @@ Zotero.Sync.Storage = new function () { Zotero.debug("No synced files have changed locally"); } - //throw ('foo'); - Zotero.DB.commitTransaction(); return changed; } @@ -622,7 +782,8 @@ Zotero.Sync.Storage = new function () { var itemID = item.id; var mode = getModeFromLibrary(item.libraryID); - if (!mode || !mode.active) { + // TODO: verify WebDAV on-demand? + if (!mode || !mode.verified) { Zotero.debug("File syncing is not active for item's library -- skipping download"); return false; } @@ -635,75 +796,40 @@ Zotero.Sync.Storage = new function () { Zotero.debug("File already exists -- replacing"); } - var setup = function () { - Zotero.Sync.Storage.EventManager.registerObserver({ - onSuccess: function () _syncInProgress = false, - - onSkip: function () _syncInProgress = false, - - onStop: function () _syncInProgress = false, - - onError: function (e) { - Zotero.Sync.Runner.setSyncIcon('error', e); - error(e); - requestCallbacks.onStop(); - } - }, false, "downloadFile"); - - try { - var queue = Zotero.Sync.Storage.QueueManager.get('download'); - - var isRunning = queue.isRunning(); - if (!isRunning) { - _syncInProgress = true; - - // Reset the sync icon - Zotero.Sync.Runner.setSyncIcon(); - } - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); + // TODO: start sync icon in cacheCredentials + return Q.fcall(function () { + return mode.cacheCredentials(); + }) + .then(function () { + // TODO: start sync icon + var library = item.libraryID; + if (!library) { + library = 0; } - return isRunning; - }; - - var run = function () { - // We have to perform setup again at the same time that we add the - // request, because otherwise a sync process could complete while - // cacheCredentials() is running and clear the event handlers. - var isRunning = setup(); + var queue = Zotero.Sync.Storage.QueueManager.get( + 'download', library + ); - try { - var queue = Zotero.Sync.Storage.QueueManager.get('download'); - - if (!requestCallbacks) { - requestCallbacks = {}; - } - var onStart = function (request) { - mode.downloadFile(request); - }; - requestCallbacks.onStart = requestCallbacks.onStart - ? [onStart, requestCallbacks.onStart] - : onStart; - - var request = new Zotero.Sync.Storage.Request( - item.libraryID + '/' + item.key, requestCallbacks - ); - - queue.addRequest(request, isRunning); + if (!requestCallbacks) { + requestCallbacks = {}; } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } - }; - - setup(); - mode.cacheCredentials(function () { - run(); + var onStart = function (request) { + return mode.downloadFile(request); + }; + requestCallbacks.onStart = requestCallbacks.onStart + ? [onStart, requestCallbacks.onStart] + : onStart; + + var request = new Zotero.Sync.Storage.Request( + library + '/' + item.key, requestCallbacks + ); + + queue.addRequest(request, true); + queue.start(); + + return request.promise; }); - - return true; } @@ -718,19 +844,19 @@ Zotero.Sync.Storage = new function () { var funcName = "Zotero.Sync.Storage.processDownload()"; if (!data) { - Zotero.Sync.Storage.EventManager.error("|data| not set in " + funcName); + throw "'data' not set in " + funcName; } if (!data.item) { - Zotero.Sync.Storage.EventManager.error("|data.item| not set in " + funcName); + throw "'data.item' not set in " + funcName; } if (!data.syncModTime) { - Zotero.Sync.Storage.EventManager.error("|data.syncModTime| not set in " + funcName); + throw "'data.syncModTime' not set in " + funcName; } if (!data.compressed && !data.syncHash) { - Zotero.Sync.Storage.EventManager.error("|data.syncHash| is required if |data.compressed| is false in " + funcName); + throw "'data.syncHash' is required if 'data.compressed' is false in " + funcName; } var item = data.item; @@ -750,13 +876,15 @@ Zotero.Sync.Storage = new function () { // and mark for updated var file = item.getFile(); if (newFile && file.leafName != newFile.leafName) { + // Bypass library access check _updatesInProgress = true; // If library isn't editable but filename was changed, update // database without updating the item's mod time, which would result // in a library access error if (!Zotero.Items.editCheck(item)) { - Zotero.debug("File renamed without library access -- updating itemAttachments path", 3); + Zotero.debug("File renamed without library access -- " + + "updating itemAttachments path", 3); item.relinkAttachmentFile(newFile, true); var useCurrentModTime = false; } @@ -779,16 +907,16 @@ Zotero.Sync.Storage = new function () { // elsewhere but the renamed file wasn't synced, so the ZIP doesn't // contain a file with the known name var missingFile = item.getFile(null, true); - Components.utils.reportError("File '" + missingFile.leafName + "' not found after processing download " + Components.utils.reportError("File '" + missingFile.leafName + + "' not found after processing download " + item.libraryID + "/" + item.key + " in " + funcName); - return; + return false; } Zotero.DB.beginTransaction(); - var syncState = Zotero.Sync.Storage.getSyncState(item.id); - - var updateItem = syncState != 1; + //var syncState = Zotero.Sync.Storage.getSyncState(item.id); + //var updateItem = syncState != this.SYNC_STATE_TO_DOWNLOAD; var updateItem = false; try { @@ -798,7 +926,7 @@ Zotero.Sync.Storage = new function () { // Reset hash and sync state Zotero.Sync.Storage.setSyncedHash(item.id, null); Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD); - _resyncOnFinish = true; + this.queueItem(item); } else { file.lastModifiedTime = syncModTime; @@ -816,29 +944,89 @@ Zotero.Sync.Storage = new function () { Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem); Zotero.DB.commitTransaction(); - _changesMade = true; + + return true; } - this.checkServer = function (modeName, callback) { - Zotero.Sync.Storage.EventManager.registerObserver({ - onSuccess: function () {}, - onError: function (e) { - Zotero.debug(e, 1); - callback(null, null, function () { - // If there's an error, just display that - Zotero.Utilities.Internal.errorPrompt(Zotero.getString('general.error'), e); - }); - return true; + this.checkServerPromise = function (mode) { + var deferred = Q.defer(); + mode.checkServer(function (uri, status) { + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var lastWin = wm.getMostRecentWindow("navigator:browser"); + + var success = mode.checkServerCallback(uri, status, lastWin, true); + if (success) { + Zotero.debug(mode.name + " file sync is successfully set up"); + Q.resolve(); + } + else { + Zotero.debug(mode.name + " verification failed"); + + var e = new Zotero.Error( + Zotero.getString('sync.storage.error.verificationFailed', mode.name), + 0, + { + dialogButtonText: Zotero.getString('sync.openSyncPreferences'), + dialogButtonCallback: function () { + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var lastWin = wm.getMostRecentWindow("navigator:browser"); + lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync'); + } + } + ); + + Q.reject(e); } - }, false, "checkServer"); - - var mode = getModeFromName(modeName); - return mode.checkServer(function (uri, status) { - callback(uri, status, function () { - mode.checkServerCallback(uri, status); - }); }); + return deferred.promise; + } + + + this.getItemDownloadImageNumber = function (item) { + var numImages = 64; + + var lk = (item.libraryID ? item.libraryID : 0) + "/" + item.key; + + if (typeof _itemDownloadPercentages[lk] == 'undefined') { + return false; + } + + var percentage = _itemDownloadPercentages[lk]; + return Math.round(percentage / 100 * (numImages - 1)) + 1; + } + + + this.setItemDownloadPercentage = function (libraryKey, percentage) { + Zotero.debug("Setting image download percentage to " + percentage + + " for item " + libraryKey); + + if (percentage !== false) { + _itemDownloadPercentages[libraryKey] = percentage; + } + else { + delete _itemDownloadPercentages[libraryKey]; + } + + var libraryID, key; + [libraryID, key] = libraryKey.split("/"); + var item = Zotero.Items.getByLibraryAndKey(libraryID, key); + Zotero.Notifier.trigger('redraw', 'item', item.id, { column: "hasAttachment" }); + + var parent = item.getSource(); + if (parent) { + var parentItem = Zotero.Items.get(parent); + var parentLibraryKey = libraryID + "/" + parentItem.key; + if (percentage !== false) { + _itemDownloadPercentages[parentLibraryKey] = percentage; + } + else { + delete _itemDownloadPercentages[parentLibraryKey]; + } + Zotero.Notifier.trigger('redraw', 'item', parentItem.id, { column: "hasAttachment" }); + } } @@ -880,9 +1068,6 @@ Zotero.Sync.Storage = new function () { this.getItemFromRequestName = function (name) { var [libraryID, key] = name.split('/'); - if (libraryID == "null") { - libraryID = null; - } return Zotero.Items.getByLibraryAndKey(libraryID, key); } @@ -890,137 +1075,29 @@ Zotero.Sync.Storage = new function () { // // Private methods // - function getModeFromName(modeName) { - return Zotero.Sync.Storage[modeName]; - } - - function getModeFromLibrary(libraryID) { if (libraryID === undefined) { throw new Error("libraryID not provided"); } // Personal library - if (libraryID === null) { - if (!Zotero.Prefs.get('sync.storage.enabled')) { - Zotero.debug('disabled'); - return false; + if (!libraryID) { + if (Zotero.Sync.Storage.ZFS.includeUserFiles) { + return Zotero.Sync.Storage.ZFS; } - - var protocol = Zotero.Prefs.get('sync.storage.protocol'); - switch (protocol) { - case 'zotero': - return getModeFromName('ZFS'); - - case 'webdav': - return getModeFromName('WebDAV'); - - default: - throw new Error("Invalid storage protocol '" + protocol + "'"); + if (Zotero.Sync.Storage.WebDAV.includeUserFiles) { + return Zotero.Sync.Storage.WebDAV; } + return false; } // Group library else { - if (!Zotero.Prefs.get('sync.storage.groups.enabled')) { - return false; + if (Zotero.Sync.Storage.ZFS.includeGroupFiles) { + return Zotero.Sync.Storage.ZFS; } - - return getModeFromName('ZFS'); - } - } - - - /** - * Starts download of all attachments marked for download - * - * @return {Boolean} - */ - function _downloadFiles(mode) { - if (!_syncInProgress) { - _syncInProgress = true; - } - - var includeUserFiles = mode.includeUserFiles && Zotero.Sync.Storage.downloadOnSync(); - var includeGroupFiles = mode.includeGroupFiles && Zotero.Sync.Storage.downloadOnSync('groups'); - - if (!includeUserFiles && !includeGroupFiles) { - Zotero.debug("No libraries are enabled for on-sync downloading"); return false; } - - var downloadFileIDs = _getFilesToDownload(includeUserFiles, includeGroupFiles); - if (!downloadFileIDs) { - Zotero.debug("No files to download"); - return false; - } - - // Check for active operations? - - var queue = Zotero.Sync.Storage.QueueManager.get('download'); - - for each(var itemID in downloadFileIDs) { - var item = Zotero.Items.get(itemID); - if (Zotero.Sync.Storage.getSyncState(itemID) != - Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD - && Zotero.Sync.Storage.isFileModified(itemID)) { - Zotero.debug("File for attachment " + itemID + " has been modified"); - Zotero.Sync.Storage.setSyncState(itemID, this.SYNC_STATE_TO_UPLOAD); - continue; - } - - var request = new Zotero.Sync.Storage.Request( - item.libraryID + '/' + item.key, - { - onStart: function (request) { - mode.downloadFile(request); - } - } - ); - queue.addRequest(request); - } - - return true; - } - - - /** - * Start upload of all attachments marked for upload - * - * @return {Boolean} - */ - function _uploadFiles(mode) { - if (!_syncInProgress) { - _syncInProgress = true; - } - - var uploadFileIDs = _getFilesToUpload(mode.includeUserFiles, mode.includeGroupFiles); - if (!uploadFileIDs) { - Zotero.debug("No files to upload"); - return false; - } - - // Check for active operations? - var queue = Zotero.Sync.Storage.QueueManager.get('upload'); - - Zotero.debug(uploadFileIDs.length + " file(s) to upload"); - - for each(var itemID in uploadFileIDs) { - var item = Zotero.Items.get(itemID); - - var request = new Zotero.Sync.Storage.Request( - item.libraryID + '/' + item.key, - { - onStart: function (request) { - mode.uploadFile(request); - } - } - ); - request.progressMax = Zotero.Attachments.getTotalFileSize(item, true); - queue.addRequest(request); - } - - return true; } @@ -1520,7 +1597,7 @@ Zotero.Sync.Storage = new function () { if (fileList.length == 0) { Zotero.debug('No files to add -- removing zip file'); tmpFile.remove(null); - request.finish(); + request.stop(); return false; } @@ -1533,6 +1610,7 @@ Zotero.Sync.Storage = new function () { return true; } catch (e) { + Zotero.debug(e, 1); request.error(e); return false; } @@ -1572,35 +1650,30 @@ Zotero.Sync.Storage = new function () { /** - * Get files marked as ready to upload + * Get files marked as ready to download * * @inner * @return {Number[]} Array of attachment itemIDs */ - function _getFilesToDownload(includeUserFiles, includeGroupFiles) { - if (!includeUserFiles && !includeGroupFiles) { - Zotero.Sync.Storage.EventManager.error( - "At least one of includeUserFiles or includeGroupFiles must be set " - + "in Zotero.Sync.Storage._getFilesToDownload()" - ); - } - + function _getFilesToDownload(libraryID, forcedOnly) { var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) " - + "WHERE syncState IN (?,?) " - // Skip attachments with empty path, which can't be saved - + "AND path!=''"; - if (includeUserFiles && !includeGroupFiles) { - sql += " AND libraryID IS NULL"; + + "WHERE libraryID=? AND syncState IN (?"; + var params = [ + libraryID == 0 ? null : libraryID, + Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD + ]; + if (!forcedOnly) { + sql += ",?"; + params.push(Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD); } - else if (!includeUserFiles && includeGroupFiles) { - sql += " AND libraryID IS NOT NULL"; + sql += ") " + // Skip attachments with empty path, which can't be saved + + "AND path!=''"; + var itemIDs = Zotero.DB.columnQuery(sql, params); + if (!itemIDs) { + return []; } - return Zotero.DB.columnQuery(sql, - [ - Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD, - Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD - ] - ); + return itemIDs; } @@ -1610,30 +1683,27 @@ Zotero.Sync.Storage = new function () { * @inner * @return {Number[]} Array of attachment itemIDs */ - function _getFilesToUpload(includeUserFiles, includeGroupFiles) { - if (!includeUserFiles && !includeGroupFiles) { - Zotero.Sync.Storage.EventManager.error( - "At least one of includeUserFiles or includeGroupFiles must be set " - + "in Zotero.Sync.Storage._getFilesToUpload()" - ); - } - + function _getFilesToUpload(libraryID) { var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) " + "WHERE syncState IN (?,?) AND linkMode IN (?,?)"; - if (includeUserFiles && !includeGroupFiles) { - sql += " AND libraryID IS NULL"; + var params = [ + Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD, + Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD, + Zotero.Attachments.LINK_MODE_IMPORTED_FILE, + Zotero.Attachments.LINK_MODE_IMPORTED_URL + ]; + if (typeof libraryID != 'undefined') { + sql += " AND libraryID=?"; + params.push(libraryID == 0 ? null : libraryID); } - else if (!includeUserFiles && includeGroupFiles) { - sql += " AND libraryID IS NOT NULL"; + else { + throw new Error("libraryID not specified"); } - return Zotero.DB.columnQuery(sql, - [ - Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD, - Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD, - Zotero.Attachments.LINK_MODE_IMPORTED_FILE, - Zotero.Attachments.LINK_MODE_IMPORTED_URL - ] - ); + var itemIDs = Zotero.DB.columnQuery(sql, params); + if (!itemIDs) { + return []; + } + return itemIDs; } @@ -1656,63 +1726,6 @@ Zotero.Sync.Storage = new function () { } - function registerDefaultObserver(modeName) { - var finish = function (cancelled, skipSuccessFile) { - // Upload success file when done - if (!_resyncOnFinish && !skipSuccessFile) { - // If we finished successfully and didn't upload any files, save the - // last sync time locally rather than setting a new one on the server, - // since we don't want other clients to check for new files - var uploadQueue = Zotero.Sync.Storage.QueueManager.get('upload', true); - var useLastSyncTime = !uploadQueue || (!cancelled && uploadQueue.lastTotalRequests == 0); - - getModeFromName(modeName).setLastSyncTime(function () { - finish(cancelled, true); - }, useLastSyncTime); - return false; - } - - Zotero.debug(modeName + " sync is complete"); - - _syncInProgress = false; - - if (_resyncOnFinish) { - Zotero.debug("Force-resyncing items in conflict"); - _resyncOnFinish = false; - Zotero.Sync.Storage.sync(modeName); - return false; - } - - if (cancelled) { - Zotero.Sync.Storage.EventManager.stop(); - } - else if (!_changesMade) { - Zotero.debug("No changes made during storage sync"); - Zotero.Sync.Storage.EventManager.skip(); - } - else { - Zotero.Sync.Storage.EventManager.success(); - } - - return true; - }; - - Zotero.Sync.Storage.EventManager.registerObserver({ - onSuccess: function () finish(), - - onSkip: function () { - _syncInProgress = false - }, - - onStop: function () finish(true), - - onError: function (e) error(e), - - onChangesMade: function () _changesMade = true - }, false, "default"); - } - - function error(e) { if (_syncInProgress) { Zotero.Sync.Storage.QueueManager.cancel(true); @@ -1808,7 +1821,7 @@ Zotero.Sync.Storage.ZipWriterObserver.prototype = { Zotero.Sync.Storage.compressionTracker.compressed += this._zipWriter.file.fileSize; Zotero.Sync.Storage.compressionTracker.uncompressed += originalSize; Zotero.debug("Average compression so far: " - + Zotero.Sync.Storage.compressionTracker.ratio + "%"); + + Math.round(Zotero.Sync.Storage.compressionTracker.ratio * 100) + "%"); this._callback(this._data); } diff --git a/chrome/content/zotero/xpcom/storage/eventLog.js b/chrome/content/zotero/xpcom/storage/eventLog.js new file mode 100644 index 000000000..05be22b7c --- /dev/null +++ b/chrome/content/zotero/xpcom/storage/eventLog.js @@ -0,0 +1,94 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2012 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 . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Sync.Storage.EventLog = (function () { + // Non-library-specific + var _general = { warnings: [], errors: [] }; + // Library-specific + var _warnings = {}; + var _errors = {}; + + function call(type, data, libraryID) { + if (libraryID) { + switch (type) { + case 'warning': + var target = _general.warnings; + break; + + case 'error': + var target = _general.errors; + break; + } + } + else { + switch (type) { + case 'warning': + var target = _warnings; + break; + + case 'error': + var target = _errors; + break; + } + } + + if (!target[libraryID]) { + target[libraryID] = []; + } + + target[libraryID].push(data); + + Zotero.debug(data, type == 'error' ? 1 : 2); + Components.utils.reportError(new Error(data)); + } + + return { + error: function (e, libraryID) call('error', e, libraryID), + warning: function (e, libraryID) call('warning', e, libraryID), + + clear: function (libraryID) { + var queues = Zotero.Sync.Storage.QueueManager.getAll(); + for each(var queue in queues) { + if (queue.isRunning()) { + Zotero.debug(queue.name[0].toUpperCase() + queue.name.substr(1) + + " queue not empty -- not clearing storage sync event observers"); + return; + } + } + + if (typeof libraryID == 'undefined') { + Zotero.debug("Clearing file sync event log"); + _general = { warnings: [], errors: [] }; + _warnings = {}; + _errors = {}; + } + else { + Zotero.debug("Clearing file sync event log for library " + libraryID); + _warnings[libraryID] = []; + _errors[libraryID] = []; + } + } + }; +}()); diff --git a/chrome/content/zotero/xpcom/storage/eventManager.js b/chrome/content/zotero/xpcom/storage/eventManager.js deleted file mode 100644 index a632579c5..000000000 --- a/chrome/content/zotero/xpcom/storage/eventManager.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - ***** 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 . - - ***** END LICENSE BLOCK ***** -*/ - -Zotero.Sync.Storage.EventManager = (function () { - var _observers = []; - - function call(handler, data, clear) { - Zotero.debug("Calling storage sync " + handler + " handlers"); - - var observers = _observers; - var cont = true; - var handled = false; - - if (clear) { - Zotero.Sync.Storage.EventManager.clear(); - } - - // Process most recently assigned observers first - for (var i = observers.length - 1; i >= 0; i--) { - let observer = observers[i].observer; - let j = i; - if (observer[handler]) { - handled = true; - if (observers[i].async) { - setTimeout(function () { - Zotero.debug("Calling " + handler + " handler " + j); - var cont = observer[handler](data); - if (cont === false) { - throw new Error("Cannot cancel events from async observer"); - } - }, 0); - } - else { - Zotero.debug("Calling " + handler + " handler " + j); - var cont = observer[handler](data); - // If handler returns explicit false, cancel further events - if (cont === false) { - break; - } - } - } - } - - if (!handled && data) { - var msg = "Unhandled storage sync event: " + data; - Zotero.debug(msg, 1); - if (handler == 'onError') { - throw new Error(msg); - } - else { - Components.utils.reportError(msg); - } - } - - // Throw errors to stop execution - if (handler == 'onError') { - if (!data) { - throw new Error("Data not provided for error"); - } - - if (cont !== false) { - throw (data); - } - } - } - - return { - registerObserver: function (observer, async, id) { - var pos = -1; - - if (id) { - for (var i = 0, len = _observers.length; i < len; i++) { - var o = _observers[i]; - if (o.id === id && o.async == async) { - pos = o; - break; - } - } - } - - if (pos == -1) { - Zotero.debug("Registering storage sync event observer '" + id + "'"); - _observers.push({ - observer: observer, - async: !!async, - id: id - }); - } - else { - Zotero.debug("Replacing storage sync event observer '" + id + "'"); - _observers[pos] = { - observer: observer, - async: !!async, - id: id - }; - } - }, - - success: function () call('onSuccess', false, true), - skip: function (clear) call('onSkip', false, true), - stop: function () call('onStop', false, true), - error: function (e) call('onError', e, true), - - warning: function (e) call('onWarning', e), - changesMade: function () call('onChangesMade'), - - clear: function () { - var queues = Zotero.Sync.Storage.QueueManager.getAll(); - for each(var queue in queues) { - if (queue.isRunning()) { - Zotero.debug(queue.name[0].toUpperCase() + queue.name.substr(1) - + " queue not empty -- not clearing storage sync event observers"); - return; - } - } - - Zotero.debug("Clearing storage sync event observers"); - _observers = []; - } - }; -}()); diff --git a/chrome/content/zotero/xpcom/storage/mode.js b/chrome/content/zotero/xpcom/storage/mode.js index b289600b4..303c517e7 100644 --- a/chrome/content/zotero/xpcom/storage/mode.js +++ b/chrome/content/zotero/xpcom/storage/mode.js @@ -26,159 +26,62 @@ Zotero.Sync.Storage.Mode = function () {}; -Zotero.Sync.Storage.Mode.prototype.__defineGetter__('enabled', function () { - try { - return this._enabled; - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } -}); - Zotero.Sync.Storage.Mode.prototype.__defineGetter__('verified', function () { - try { - return this._verified; - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } -}); - -Zotero.Sync.Storage.Mode.prototype.__defineGetter__('active', function () { - try { - return this._enabled && this._verified && this._initFromPrefs(); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._verified; }); Zotero.Sync.Storage.Mode.prototype.__defineGetter__('username', function () { - try { - return this._username; - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._username; }); Zotero.Sync.Storage.Mode.prototype.__defineGetter__('password', function () { - try { - return this._password; - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._password; }); Zotero.Sync.Storage.Mode.prototype.__defineSetter__('password', function (val) { - try { - this._password = val; - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + this._password = val; }); Zotero.Sync.Storage.Mode.prototype.init = function () { - try { - return this._init(); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } -} - -Zotero.Sync.Storage.Mode.prototype.initFromPrefs = function () { - try { - return this._initFromPrefs(); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._init(); } Zotero.Sync.Storage.Mode.prototype.sync = function (observer) { - Zotero.Sync.Storage.sync(this.name, observer); + return Zotero.Sync.Storage.sync(this.name, observer); } Zotero.Sync.Storage.Mode.prototype.downloadFile = function (request) { - try { - this._downloadFile(request); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._downloadFile(request); } Zotero.Sync.Storage.Mode.prototype.uploadFile = function (request) { - try { - this._uploadFile(request); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._uploadFile(request); } -Zotero.Sync.Storage.Mode.prototype.getLastSyncTime = function (callback) { - try { - this._getLastSyncTime(callback); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } +Zotero.Sync.Storage.Mode.prototype.getLastSyncTime = function (libraryID) { + return this._getLastSyncTime(libraryID); } Zotero.Sync.Storage.Mode.prototype.setLastSyncTime = function (callback, useLastSyncTime) { - try { - this._setLastSyncTime(callback, useLastSyncTime); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._setLastSyncTime(callback, useLastSyncTime); } Zotero.Sync.Storage.Mode.prototype.checkServer = function (callback) { - try { - return this._checkServer(callback); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._checkServer(callback); } Zotero.Sync.Storage.Mode.prototype.checkServerCallback = function (uri, status, window, skipSuccessMessage) { - try { - return this._checkServerCallback(uri, status, window, skipSuccessMessage); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._checkServerCallback(uri, status, window, skipSuccessMessage); } Zotero.Sync.Storage.Mode.prototype.cacheCredentials = function (callback) { - try { - return this._cacheCredentials(callback); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._cacheCredentials(callback); } Zotero.Sync.Storage.Mode.prototype.purgeDeletedStorageFiles = function (callback) { - try { - this._purgeDeletedStorageFiles(callback); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._purgeDeletedStorageFiles(callback); } Zotero.Sync.Storage.Mode.prototype.purgeOrphanedStorageFiles = function (callback) { - try { - this._purgeOrphanedStorageFiles(callback); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return this._purgeOrphanedStorageFiles(callback); } diff --git a/chrome/content/zotero/xpcom/storage/queue.js b/chrome/content/zotero/xpcom/storage/queue.js index df5ff5a4b..21956a009 100644 --- a/chrome/content/zotero/xpcom/storage/queue.js +++ b/chrome/content/zotero/xpcom/storage/queue.js @@ -26,13 +26,14 @@ /** * Queue for storage sync transfer requests * - * @param {String} name Queue name (e.g., 'download' or 'upload') + * @param {String} type Queue type (e.g., 'download' or 'upload') */ -Zotero.Sync.Storage.Queue = function (name) { - Zotero.debug("Initializing " + name + " queue"); +Zotero.Sync.Storage.Queue = function (type, libraryID) { + Zotero.debug("Initializing " + type + " queue for library " + libraryID); // Public properties - this.name = name; + this.type = type; + this.libraryID = libraryID; this.maxConcurrentRequests = 1; this.activeRequests = 0; this.totalRequests = 0; @@ -42,16 +43,25 @@ Zotero.Sync.Storage.Queue = function (name) { this._highPriority = []; this._running = false; this._stopping = false; + this._finished = false; + this._error = false; this._finishedReqs = 0; - this._lastTotalRequests = 0; + this._localChanges = false; + this._remoteChanges = false; + this._conflicts = []; } -Zotero.Sync.Storage.Queue.prototype.__defineGetter__('Name', function () { - return this.name[0].toUpperCase() + this.name.substr(1); +Zotero.Sync.Storage.Queue.prototype.__defineGetter__('name', function () { + return this.type + "/" + this.libraryID; +}); + +Zotero.Sync.Storage.Queue.prototype.__defineGetter__('Type', function () { + return this.type[0].toUpperCase() + this.type.substr(1); }); Zotero.Sync.Storage.Queue.prototype.__defineGetter__('running', function () this._running); Zotero.Sync.Storage.Queue.prototype.__defineGetter__('stopping', function () this._stopping); +Zotero.Sync.Storage.Queue.prototype.__defineGetter__('finished', function () this._finished); Zotero.Sync.Storage.Queue.prototype.__defineGetter__('unfinishedRequests', function () { return this.totalRequests - this.finishedRequests; @@ -73,22 +83,51 @@ Zotero.Sync.Storage.Queue.prototype.__defineSetter__('finishedRequests', functio // Last request if (val == this.totalRequests) { - Zotero.debug(this.Name + " queue is done"); + Zotero.debug(this.Type + " queue is done for library " + this.libraryID); // DEBUG info Zotero.debug("Active requests: " + this.activeRequests); if (this.activeRequests) { - throw new Error(this.Name + " queue can't be done if there are active requests"); + throw new Error(this.Type + " queue for library " + this.libraryID + + " can't be done if there are active requests"); } this._running = false; this._stopping = false; + this._finished = true; this._requests = {}; this._highPriority = []; - this._finishedReqs = 0; - this._lastTotalRequests = this.totalRequests; - this.totalRequests = 0; + + var localChanges = this._localChanges; + var remoteChanges = this._remoteChanges; + var conflicts = this._conflicts.concat(); + this._localChanges = false; + this._remoteChanges = false; + this._conflicts = []; + + if (!this._error) { + Zotero.debug("Resolving promise for queue " + this.name); + Zotero.debug(this._localChanges); + Zotero.debug(this._remoteChanges); + Zotero.debug(this._conflicts); + + this._deferred.resolve({ + libraryID: this.libraryID, + type: this.type, + localChanges: localChanges, + remoteChanges: remoteChanges, + conflicts: conflicts + }); + } + else { + Zotero.debug("Rejecting promise for queue " + this.name); + var e = this._error; + this._error = false; + e.libraryID = this.libraryID; + e.type = this.type; + this._deferred.reject(e); + } return; } @@ -99,10 +138,6 @@ Zotero.Sync.Storage.Queue.prototype.__defineSetter__('finishedRequests', functio this.advance(); }); -Zotero.Sync.Storage.Queue.prototype.__defineGetter__('lastTotalRequests', function () { - return this._lastTotalRequests; -}); - Zotero.Sync.Storage.Queue.prototype.__defineGetter__('queuedRequests', function () { return this.unfinishedRequests - this.activeRequests; }); @@ -119,6 +154,9 @@ Zotero.Sync.Storage.Queue.prototype.__defineGetter__('percentage', function () { if (this.totalRequests == 0) { return 0; } + if (this._finished) { + return 100; + } var completedRequests = 0; for each(var request in this._requests) { @@ -144,9 +182,13 @@ Zotero.Sync.Storage.Queue.prototype.isStopping = function () { * @param {Boolean} highPriority Add or move request to high priority queue */ Zotero.Sync.Storage.Queue.prototype.addRequest = function (request, highPriority) { + if (this._finished) { + this.reset(); + } + request.queue = this; var name = request.name; - Zotero.debug("Queuing " + this.name + " request '" + name + "'"); + Zotero.debug("Queuing " + this.type + " request '" + name + "' for library " + this.libraryID); if (this._requests[name]) { if (highPriority) { @@ -166,56 +208,133 @@ Zotero.Sync.Storage.Queue.prototype.addRequest = function (request, highPriority if (highPriority) { this._highPriority.push(name); } - - this.advance(); } +Zotero.Sync.Storage.Queue.prototype.start = function () { + if (!this._deferred || this._deferred.promise.isResolved()) { + Zotero.debug("Creating deferred for queue " + this.name); + this._deferred = Q.defer(); + } + // The queue manager needs to know what queues were running in the + // current session + Zotero.Sync.Storage.QueueManager.addCurrentQueue(this); + this.advance(); + return this._deferred.promise; +} + + + /** * Start another request in this queue if there's an available slot */ Zotero.Sync.Storage.Queue.prototype.advance = function () { this._running = true; + this._finished = false; if (this._stopping) { - Zotero.debug(this.Name + " queue is being stopped in " - + "Zotero.Sync.Storage.Queue.advance()", 2); + Zotero.debug(this.Type + " queue for library " + this.libraryID + + "is being stopped in Zotero.Sync.Storage.Queue.advance()", 2); return; } if (!this.queuedRequests) { - Zotero.debug("No remaining requests in " + this.name + " queue (" + Zotero.debug("No remaining requests in " + this.type + + " queue for library " + this.libraryID + " (" + this.activeRequests + " active, " + this.finishedRequests + " finished)"); return; } if (this.activeRequests >= this.maxConcurrentRequests) { - Zotero.debug(this.Name + " queue is busy (" - + this.activeRequests + "/" + this.maxConcurrentRequests + ")"); + Zotero.debug(this.Type + " queue for library " + this.libraryID + + " is busy (" + this.activeRequests + "/" + + this.maxConcurrentRequests + ")"); return; } + + // Start the first unprocessed request // Try the high-priority queue first - var name, request; + var self = this; + var request, name; while (name = this._highPriority.shift()) { request = this._requests[name]; - if (!request.isRunning() && !request.isFinished()) { - request.start(); - this.advance(); - return; + if (request.isRunning() || request.isFinished()) { + continue; } + + let requestName = name; + + Q.fcall(function () { + var promise = request.start(); + self.advance(); + return promise; + }) + .then(function (result) { + if (result.localChanges) { + self._localChanges = true; + } + if (result.remoteChanges) { + self._remoteChanges = true; + } + if (result.conflict) { + self.addConflict( + requestName, + result.conflict.local, + result.conflict.remote + ); + } + }) + .fail(function (e) { + self.error(e); + }); + + return; } // And then others - for each(request in this._requests) { - if (!request.isRunning() && !request.isFinished()) { - request.start(); - this.advance(); - return; + for each(var request in this._requests) { + if (request.isRunning() || request.isFinished()) { + continue; } + + let requestName = request.name; + + // This isn't in an fcall() because the request needs to get marked + // as running immediately so that it doesn't get run again by a + // subsequent advance() call. + try { + var promise = request.start(); + self.advance(); + } + catch (e) { + self.error(e); + } + + Q.when(promise) + .then(function (result) { + if (result.localChanges) { + self._localChanges = true; + } + if (result.remoteChanges) { + self._remoteChanges = true; + } + if (result.conflict) { + self.addConflict( + requestName, + result.conflict.local, + result.conflict.remote + ); + } + }) + .fail(function (e) { + self.error(e); + }); + + return; } } @@ -225,8 +344,26 @@ Zotero.Sync.Storage.Queue.prototype.updateProgress = function () { } +Zotero.Sync.Storage.Queue.prototype.addConflict = function (requestName, localData, remoteData) { + Zotero.debug('==========='); + Zotero.debug(localData); + Zotero.debug(remoteData); + + this._conflicts.push({ + name: requestName, + localData: localData, + remoteData: remoteData + }); +} + + Zotero.Sync.Storage.Queue.prototype.error = function (e) { - Zotero.Sync.Storage.EventManager.error(e); + if (!this._error) { + this._error = e; + } + Zotero.debug(e, 1); + Components.utils.reportError(e.message ? e.message : e); + this.stop(); } @@ -235,14 +372,18 @@ Zotero.Sync.Storage.Queue.prototype.error = function (e) { */ Zotero.Sync.Storage.Queue.prototype.stop = function () { if (!this._running) { - Zotero.debug(this.Name + " queue is not running"); + Zotero.debug(this.Type + " queue for library " + this.libraryID + + " is not running"); return; } if (this._stopping) { - Zotero.debug("Already stopping " + this.name + " queue"); + Zotero.debug("Already stopping " + this.type + " queue for library " + + this.libraryID); return; } + Zotero.debug("Stopping " + this.type + " queue for library " + this.libraryID); + // If no requests, finish manually /*if (this.activeRequests == 0) { this._finishedRequests = this._finishedRequests; @@ -255,4 +396,13 @@ Zotero.Sync.Storage.Queue.prototype.stop = function () { request.stop(); } } + + Zotero.debug("Queue is stopped"); +} + + +Zotero.Sync.Storage.Queue.prototype.reset = function () { + this._finished = false; + this._finishedReqs = 0; + this.totalRequests = 0; } diff --git a/chrome/content/zotero/xpcom/storage/queueManager.js b/chrome/content/zotero/xpcom/storage/queueManager.js index a2732a8bb..ba8e7c437 100644 --- a/chrome/content/zotero/xpcom/storage/queueManager.js +++ b/chrome/content/zotero/xpcom/storage/queueManager.js @@ -26,8 +26,63 @@ Zotero.Sync.Storage.QueueManager = new function () { var _queues = {}; - var _conflicts = []; - var _cancelled = false; + var _currentQueues = []; + + this.start = function (libraryID) { + if (libraryID === 0 || libraryID) { + var queues = this.getAll(libraryID); + } + else { + var queues = this.getAll(); + } + + Zotero.debug("Starting file sync queues"); + + var promises = []; + for each(var queue in queues) { + if (!queue.unfinishedRequests) { + continue; + } + Zotero.debug("Starting queue " + queue.name); + promises.push(queue.start()); + } + + if (!promises.length) { + Zotero.debug("No files to sync"); + } + + return Q.allResolved(promises) + .then(function (promises) { + Zotero.debug("All storage queues are finished"); + promises.forEach(function (promise) { + if (promise.isFulfilled()) { + var result = promise.valueOf(); + if (result.conflicts.length) { + Zotero.debug("Reconciling conflicts for library " + result.libraryID); + Zotero.debug(result.conflicts); + var data = _reconcileConflicts(result.conflicts); + if (data) { + _processMergeData(data); + } + } + } + }); + + return promises; + }); + }; + + this.stop = function (libraryID) { + if (libraryID === 0 || libraryID) { + var queues = this.getAll(libraryID); + } + else { + var queues = this.getAll(); + } + for (var queue in queues) { + queue.stop(); + } + }; /** @@ -35,13 +90,19 @@ Zotero.Sync.Storage.QueueManager = new function () { * * @param {String} queueName */ - this.get = function (queueName, noInit) { + this.get = function (queueName, libraryID, noInit) { + if (typeof libraryID == 'undefined') { + throw new Error("libraryID not specified"); + } + + var hash = queueName + "/" + libraryID; + // Initialize the queue if it doesn't exist yet - if (!_queues[queueName]) { + if (!_queues[hash]) { if (noInit) { return false; } - var queue = new Zotero.Sync.Storage.Queue(queueName); + var queue = new Zotero.Sync.Storage.Queue(queueName, libraryID); switch (queueName) { case 'download': queue.maxConcurrentRequests = @@ -56,22 +117,36 @@ Zotero.Sync.Storage.QueueManager = new function () { default: throw ("Invalid queue '" + queueName + "' in Zotero.Sync.Storage.QueueManager.get()"); } - _queues[queueName] = queue; + _queues[hash] = queue; } - return _queues[queueName]; - } + return _queues[hash]; + }; - this.getAll = function () { + this.getAll = function (libraryID) { var queues = []; for each(var queue in _queues) { - queues.push(queue); + if (typeof libraryID == 'undefined' || queue.libraryID === libraryID) { + queues.push(queue); + } } return queues; }; + this.addCurrentQueue = function (queue) { + if (!this.hasCurrentQueue(queue)) { + _currentQueues.push(queue.name); + } + } + + + this.hasCurrentQueue = function (queue) { + return _currentQueues.indexOf(queue.name) != -1; + } + + /** * Stop all queues * @@ -81,7 +156,6 @@ Zotero.Sync.Storage.QueueManager = new function () { */ this.cancel = function (skipStorageFinish) { Zotero.debug("Stopping all storage queues"); - _cancelled = true; for each(var queue in _queues) { if (queue.isRunning() && !queue.isStopping()) { queue.stop(); @@ -92,26 +166,7 @@ Zotero.Sync.Storage.QueueManager = new function () { this.finish = function () { Zotero.debug("All storage queues are finished"); - - if (!_cancelled && _conflicts.length) { - var data = _reconcileConflicts(); - if (data) { - _processMergeData(data); - } - } - - try { - if (_cancelled) { - Zotero.Sync.Storage.EventManager.stop(); - } - else { - Zotero.Sync.Storage.EventManager.success(); - } - } - finally { - _cancelled = false; - _conflicts = []; - } + _currentQueues = []; } @@ -132,86 +187,32 @@ Zotero.Sync.Storage.QueueManager = new function () { activeRequests += queue.activeRequests; } if (activeRequests == 0) { - this.updateProgressMeters(0); + _updateProgressMeters(0); if (allFinished) { this.finish(); } return; } - // Percentage - var percentageSum = 0; - var numQueues = 0; + var status = {}; for each(var queue in _queues) { - percentageSum += queue.percentage; - numQueues++; - } - var percentage = Math.round(percentageSum / numQueues); - //Zotero.debug("Total percentage is " + percentage); - - // Remaining KB - var downloadStatus = _queues.download ? - _getQueueStatus(_queues.download) : 0; - var uploadStatus = _queues.upload ? - _getQueueStatus(_queues.upload) : 0; - - this.updateProgressMeters( - activeRequests, percentage, downloadStatus, uploadStatus - ); - } - - - /** - * Cycle through windows, updating progress meters with new values - */ - this.updateProgressMeters = function (activeRequests, percentage, downloadStatus, uploadStatus) { - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - var enumerator = wm.getEnumerator("navigator:browser"); - while (enumerator.hasMoreElements()) { - var win = enumerator.getNext(); - if (!win.ZoteroPane) continue; - var doc = win.ZoteroPane.document; - - // - // TODO: Move to overlay.js? - // - var box = doc.getElementById("zotero-tb-sync-progress-box"); - var meter = doc.getElementById("zotero-tb-sync-progress"); - - if (activeRequests == 0) { - box.hidden = true; + if (!this.hasCurrentQueue(queue)) { continue; } - meter.setAttribute("value", percentage); - box.hidden = false; - - var tooltip = doc. - getElementById("zotero-tb-sync-progress-tooltip-progress"); - tooltip.setAttribute("value", percentage + "%"); - - var tooltip = doc. - getElementById("zotero-tb-sync-progress-tooltip-downloads"); - tooltip.setAttribute("value", downloadStatus); - - var tooltip = doc. - getElementById("zotero-tb-sync-progress-tooltip-uploads"); - tooltip.setAttribute("value", uploadStatus); + if (!status[queue.libraryID]) { + status[queue.libraryID] = {}; + } + if (!status[queue.libraryID][queue.type]) { + status[queue.libraryID][queue.type] = {}; + } + status[queue.libraryID][queue.type].statusString = _getQueueStatus(queue); + status[queue.libraryID][queue.type].percentage = queue.percentage; + status[queue.libraryID][queue.type].totalRequests = queue.totalRequests; + status[queue.libraryID][queue.type].finished = queue.finished; } - } - - - this.addConflict = function (requestName, localData, remoteData) { - Zotero.debug('==========='); - Zotero.debug(localData); - Zotero.debug(remoteData); - _conflicts.push({ - name: requestName, - localData: localData, - remoteData: remoteData - }); + _updateProgressMeters(activeRequests, status); } @@ -226,26 +227,76 @@ Zotero.Sync.Storage.QueueManager = new function () { var unfinishedRequests = queue.unfinishedRequests; if (!unfinishedRequests) { - return Zotero.getString('sync.storage.none') + return Zotero.getString('sync.storage.none'); } - var kbRemaining = Zotero.getString( - 'sync.storage.kbRemaining', - Zotero.Utilities.numberFormat(remaining / 1024, 0) - ); + if (remaining > 1000) { + var bytesRemaining = Zotero.getString( + 'sync.storage.mbRemaining', + Zotero.Utilities.numberFormat(remaining / 1000 / 1000, 1) + ); + } + else { + var bytesRemaining = Zotero.getString( + 'sync.storage.kbRemaining', + Zotero.Utilities.numberFormat(remaining / 1000, 0) + ); + } var totalRequests = queue.totalRequests; var filesRemaining = Zotero.getString( 'sync.storage.filesRemaining', [totalRequests - unfinishedRequests, totalRequests] ); - var status = Zotero.localeJoin([kbRemaining, '(' + filesRemaining + ')']); - return status; + return bytesRemaining + ' (' + filesRemaining + ')'; + } + + /** + * Cycle through windows, updating progress meters with new values + */ + function _updateProgressMeters(activeRequests, status) { + // Get overall percentage across queues + var sum = 0, num = 0, percentage, total; + for each(var libraryStatus in status) { + for each(var queueStatus in libraryStatus) { + percentage = queueStatus.percentage; + total = queueStatus.totalRequests; + sum += total * percentage; + num += total; + } + } + var percentage = Math.round(sum / num); + + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var enumerator = wm.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + var win = enumerator.getNext(); + if (!win.ZoteroPane) continue; + var doc = win.ZoteroPane.document; + + var box = doc.getElementById("zotero-tb-sync-progress-box"); + var meter = doc.getElementById("zotero-tb-sync-progress"); + + if (activeRequests == 0) { + box.hidden = true; + continue; + } + + meter.setAttribute("value", percentage); + box.hidden = false; + + var percentageLabel = doc.getElementById('zotero-tb-sync-progress-tooltip-progress'); + percentageLabel.lastChild.setAttribute('value', percentage + "%"); + + var statusBox = doc.getElementById('zotero-tb-sync-progress-status'); + statusBox.data = status; + } } - function _reconcileConflicts() { + function _reconcileConflicts(conflicts) { var objectPairs = []; - for each(var conflict in _conflicts) { + for each(var conflict in conflicts) { var item = Zotero.Sync.Storage.getItemFromRequestName(conflict.name); var item1 = item.clone(false, false, true); item1.setField('dateModified', @@ -279,8 +330,8 @@ Zotero.Sync.Storage.QueueManager = new function () { // Since we're only putting cloned items into the merge window, // we have to manually set the ids - for (var i=0; i<_conflicts.length; i++) { - io.dataOut[i].id = Zotero.Sync.Storage.getItemFromRequestName(_conflicts[i].name).id; + for (var i=0; i 100) { Zotero.debug(percentage + " is greater than 100 for " - + this.name + " request", 2); + + "request " + this.name, 2); Zotero.debug(this.progress); Zotero.debug(this.progressMax); percentage = 100; @@ -119,7 +135,15 @@ Zotero.Sync.Storage.Request.prototype.__defineGetter__('percentage', function () 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; } @@ -151,22 +175,70 @@ Zotero.Sync.Storage.Request.prototype.setChannel = function (channel) { Zotero.Sync.Storage.Request.prototype.start = function () { if (!this.queue) { - throw ("Request '" + this.name + "' must be added to a queue before starting"); + 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 ("Request '" + this.name + "' already running in " - + "Zotero.Sync.Storage.Request.start()"); + throw new Error("Request " + this.name + " already running"); } - Zotero.debug("Starting " + this.queue.name + " request '" + this.name + "'"); this._running = true; this.queue.activeRequests++; - if (this._onStart) { - for each(var f in this._onStart) { - f(this); - } + + 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; } @@ -191,6 +263,8 @@ Zotero.Sync.Storage.Request.prototype.isFinished = function () { * (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() " @@ -219,6 +293,10 @@ Zotero.Sync.Storage.Request.prototype.onProgress = function (channel, 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); @@ -227,62 +305,48 @@ Zotero.Sync.Storage.Request.prototype.onProgress = function (channel, progress, } -Zotero.Sync.Storage.Request.prototype.error = function (e) { - this.queue.error(e); -} - - /** * Stop the request's underlying network request, if there is one */ Zotero.Sync.Storage.Request.prototype.stop = function () { - var finishNow = false; - try { - // If upload already finished, finish() will never be called otherwise - if (this.channel) { - this.channel.QueryInterface(Components.interfaces.nsIHttpChannel); - // Throws error if request not finished - this.channel.requestSucceeded; - Zotero.debug("Channel is no longer running for request " + this.name); - Zotero.debug(this.channel.requestSucceeded); - finishNow = true; + if (this.channel) { + try { + Zotero.debug("Stopping request '" + this.name + "'"); + this.channel.cancel(0x804b0002); // NS_BINDING_ABORTED + } + catch (e) { + Zotero.debug(e); } } - catch (e) {} - - if (!this._running || !this.channel || finishNow) { - this.finish(); - return; + else { + this._finish(); } - - Zotero.debug("Stopping request '" + this.name + "'"); - this.channel.cancel(0x804b0002); // NS_BINDING_ABORTED } /** * Mark request as finished and notify queue that it's done */ -Zotero.Sync.Storage.Request.prototype.finish = function () { - if (this._finished) { - throw ("Request '" + this.name + "' is already finished"); - } - +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--; } - // mechanism for failures? - this.queue.finishedRequests++; - this.queue.updateProgress(); - - if (this._onStop) { - for each(var f in this._onStop) { - f(); - } + // 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; } } diff --git a/chrome/content/zotero/xpcom/storage/webdav.js b/chrome/content/zotero/xpcom/storage/webdav.js index 20b07eed9..ca6c29979 100644 --- a/chrome/content/zotero/xpcom/storage/webdav.js +++ b/chrome/content/zotero/xpcom/storage/webdav.js @@ -30,6 +30,7 @@ Zotero.Sync.Storage.WebDAV = (function () { var _defaultError = "A WebDAV file sync error occurred. Please try syncing again.\n\nIf you receive this message repeatedly, check your WebDAV server settings in the Sync pane of the Zotero preferences."; var _defaultErrorRestart = "A WebDAV file sync error occurred. Please restart " + Zotero.appName + " and try syncing again.\n\nIf you receive this message repeatedly, check your WebDAV server settings in the Sync pane of the Zotero preferences."; + var _initialized = false; var _parentURI; var _rootURI; var _cachedCredentials = false; @@ -46,85 +47,84 @@ Zotero.Sync.Storage.WebDAV = (function () { * @param {Zotero.Item} item * @param {Function} callback Callback f(item, mdate) */ - function getStorageModificationTime(item, callback) { + function getStorageModificationTime(item) { var uri = getItemPropertyURI(item); - Zotero.HTTP.doGet(uri, function (req) { - checkResponse(req); - - var funcName = "Zotero.Sync.Storage.WebDAV.getStorageModificationTime()"; - - // mod_speling can return 300s for 404s with base name matches - if (req.status == 404 || req.status == 300) { - callback(item, false); - return; - } - else if (req.status != 200) { - Zotero.debug(req.responseText); - Zotero.Sync.Storage.EventManager.error( - "Unexpected status code " + req.status + " in " + funcName - ); - } - - Zotero.debug(req.responseText); - - // No modification time set - if (!req.responseText) { - callback(item, false); - return; - } - - try { - var xml = new XML(req.responseText); - } - catch (e) { - Zotero.debug(e); - var xml = null; - } - - if (xml) { - Zotero.debug(xml.children().length()); - } - - if (xml && xml.children().length()) { - // TODO: other stuff, but this makes us forward-compatible - mtime = xml.mtime.toString(); - var seconds = false; - } - else { - mtime = req.responseText; - var seconds = true; - } - - var invalid = false; - - // Unix timestamps need to be converted to ms-based timestamps - if (seconds) { - if (mtime.match(/^[0-9]{1,10}$/)) { - Zotero.debug("Converting Unix timestamp '" + mtime + "' to milliseconds"); - mtime = mtime * 1000; + return Zotero.HTTP.promise( + "GET", uri, { debug: true, successCodes: [200, 300, 404] } + ) + .then(function (req) { + checkResponse(req); + + var funcName = "Zotero.Sync.Storage.WebDAV.getStorageModificationTime()"; + + // mod_speling can return 300s for 404s with base name matches + if (req.status == 404 || req.status == 300) { + return false; + } + + // No modification time set + if (!req.responseText) { + return false; + } + + try { + var xml = new XML(req.responseText); + } + catch (e) { + Zotero.debug(e); + var xml = null; + } + + if (xml) { + Zotero.debug(xml.children().length()); + } + + if (xml && xml.children().length()) { + // TODO: other stuff, but this makes us forward-compatible + mtime = xml.mtime.toString(); + var seconds = false; } else { + mtime = req.responseText; + var seconds = true; + } + + var invalid = false; + + // Unix timestamps need to be converted to ms-based timestamps + if (seconds) { + if (mtime.match(/^[0-9]{1,10}$/)) { + Zotero.debug("Converting Unix timestamp '" + mtime + "' to milliseconds"); + mtime = mtime * 1000; + } + else { + invalid = true; + } + } + else if (!mtime.match(/^[0-9]{1,13}$/)) { invalid = true; } - } - else if (!mtime.match(/^[0-9]{1,13}$/)) { - invalid = true; - } - - // Delete invalid .prop files - if (invalid) { - var msg = "Invalid mod date '" + Zotero.Utilities.ellipsize(mtime, 20) - + "' for item " + Zotero.Items.getLibraryKeyHash(item); - Zotero.debug(msg, 1); - Components.utils.reportError(msg); - deleteStorageFiles([item.key + ".prop"]); - Zotero.Sync.Storage.EventManager.error(_defaultError); - } - - var mdate = new Date(parseInt(mtime)); - callback(item, mdate); - }); + + // Delete invalid .prop files + if (invalid) { + var msg = "Invalid mod date '" + Zotero.Utilities.ellipsize(mtime, 20) + + "' for item " + Zotero.Items.getLibraryKeyHash(item); + Zotero.debug(msg, 1); + Components.utils.reportError(msg); + deleteStorageFiles([item.key + ".prop"]); + throw _defaultError; + } + + return new Date(parseInt(mtime)); + }) + .fail(function (e) { + if (e instanceof Zotero.HTTP.UnexpectedStatusException) { + Zotero.debug(req.responseText); + throw new Error("Unexpected status code " + e.status + " in " + funcName); + } + throw e; + }); } @@ -132,9 +132,8 @@ Zotero.Sync.Storage.WebDAV = (function () { * Set mod time of file on storage server * * @param {Zotero.Item} item - * @param {Function} callback Callback f(item, props) */ - function setStorageModificationTime(item, callback) { + function setStorageModificationTime(item) { var uri = getItemPropertyURI(item); var mtime = item.attachmentModificationTime; @@ -145,202 +144,198 @@ Zotero.Sync.Storage.WebDAV = (function () { {hash} ; - Zotero.HTTP.WebDAV.doPut(uri, prop.toXMLString(), function (req) { - switch (req.status) { - case 200: - case 201: - case 204: - break; - - default: - Zotero.debug(req.responseText); - throw new Error("Unexpected status code " + req.status); - } - callback(item, { mtime: mtime, hash: hash }); - }); - - - /** - * Upload the generated ZIP file to the server - * - * @param {Object} Object with 'request' property - * @return {void} - */ - function processUploadFile(data) { - /* - updateSizeMultiplier( - (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100 - ); - */ - var request = data.request; - var item = Zotero.Sync.Storage.getItemFromRequestName(request.name); - - getStorageModificationTime(item, function (item, mdate) { - try { - if (!request.isRunning()) { - Zotero.debug("Upload request '" + request.name - + "' is no longer running after getting mod time"); - return; - } - - // Check for conflict - if (Zotero.Sync.Storage.getSyncState(item.id) - != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) { - if (mdate) { - // Remote prop time - var mtime = mdate.getTime(); - - // Local file time - var fmtime = item.attachmentModificationTime; - - var same = false; - if (fmtime == mtime) { - same = true; - Zotero.debug("File mod time matches remote file -- skipping upload"); - } - // Allow floored timestamps for filesystems that don't support - // millisecond precision (e.g., HFS+) - else if (Math.floor(mtime / 1000) * 1000 == fmtime || Math.floor(fmtime / 1000) * 1000 == mtime) { - same = true; - Zotero.debug("File mod times are within one-second precision (" + fmtime + " ≅ " + mtime + ") " - + "-- skipping upload"); - } - // Allow timestamp to be exactly one hour off to get around - // time zone issues -- there may be a proper way to fix this - else if (Math.abs(fmtime - mtime) == 3600000 - // And check with one-second precision as well - || Math.abs(fmtime - Math.floor(mtime / 1000) * 1000) == 3600000 - || Math.abs(Math.floor(fmtime / 1000) * 1000 - mtime) == 3600000) { - same = true; - Zotero.debug("File mod time (" + fmtime + ") is exactly one hour off remote file (" + mtime + ") " - + "-- assuming time zone issue and skipping upload"); - } - - if (same) { - Zotero.DB.beginTransaction(); - var syncState = Zotero.Sync.Storage.getSyncState(item.id); - Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime, true); - Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC); - Zotero.DB.commitTransaction(); - onChangesMade(); - request.finish(); - return; - } - - var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id); - if (smtime != mtime) { - var localData = { modTime: fmtime }; - var remoteData = { modTime: mtime }; - Zotero.Sync.Storage.QueueManager.addConflict( - request.name, localData, remoteData - ); - Zotero.debug("Conflict -- last synced file mod time " - + "does not match time on storage server" - + " (" + smtime + " != " + mtime + ")"); - request.finish(); - return; - } - } - else { - Zotero.debug("Remote file not found for item " + item.id); - } - } - - var file = Zotero.getTempDirectory(); - file.append(item.key + '.zip'); - - var fis = Components.classes["@mozilla.org/network/file-input-stream;1"] - .createInstance(Components.interfaces.nsIFileInputStream); - fis.init(file, 0x01, 0, 0); - - var bis = Components.classes["@mozilla.org/network/buffered-input-stream;1"] - .createInstance(Components.interfaces.nsIBufferedInputStream) - bis.init(fis, 64 * 1024); - - var uri = getItemURI(item); - - var ios = Components.classes["@mozilla.org/network/io-service;1"]. - getService(Components.interfaces.nsIIOService); - var channel = ios.newChannelFromURI(uri); - channel.QueryInterface(Components.interfaces.nsIUploadChannel); - channel.setUploadStream(bis, 'application/octet-stream', -1); - channel.QueryInterface(Components.interfaces.nsIHttpChannel); - channel.requestMethod = 'PUT'; - channel.allowPipelining = false; - - channel.setRequestHeader('Keep-Alive', '', false); - channel.setRequestHeader('Connection', '', false); - - var listener = new Zotero.Sync.Storage.StreamListener( - { - onProgress: function (a, b, c) { - request.onProgress(a, b, c); - }, - onStop: function (httpRequest, status, response, data) { onUploadComplete(httpRequest, status, response,data); }, - onCancel: function (httpRequest, status, data) { onUploadCancel(httpRequest, status, data); }, - request: request, - item: item, - streams: [fis, bis] - } - ); - channel.notificationCallbacks = listener; - - var dispURI = uri.clone(); - if (dispURI.password) { - dispURI.password = '********'; - } - Zotero.debug("HTTP PUT of " + file.leafName + " to " + dispURI.spec); - - channel.asyncOpen(listener, null); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return Zotero.HTTP.promise("PUT", uri, prop.toXMLString(), + { debug: true, successCodes: [200, 201, 204] }) + .then(function (req) { + return { mtime: mtime, hash: hash }; + }) + .fail(function (e) { + throw new Error("Unexpected status code " + e.xmlhttp.status); }); - } + }; + + + + /** + * Upload the generated ZIP file to the server + * + * @param {Object} Object with 'request' property + * @return {void} + */ + function processUploadFile(data) { + /* + updateSizeMultiplier( + (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100 + ); + */ + var request = data.request; + var item = Zotero.Sync.Storage.getItemFromRequestName(request.name); - - function onUploadComplete(httpRequest, status, response, data) { - var request = data.request; - var item = data.item; - var url = httpRequest.name; - - Zotero.debug("Upload of attachment " + item.key - + " finished with status code " + status); - - switch (status) { - case 200: - case 201: - case 204: - break; - - case 403: - case 500: - Zotero.debug(response); - Zotero.Sync.Storage.EventManager.error( - Zotero.getString('sync.storage.error.fileUploadFailed') - + " " + Zotero.getString('sync.storage.error.checkFileSyncSettings') - ); - - case 507: - Zotero.debug(response); - Zotero.Sync.Storage.EventManager.error( - Zotero.getString('sync.storage.error.webdav.insufficientSpace') - ); - - default: - Zotero.debug(response); - Zotero.Sync.Storage.EventManager.error( - "Unexpected file upload status " + status - + " in Zotero.Sync.Storage.WebDAV.onUploadComplete()" - ); - } - - setStorageModificationTime(item, function (item, props) { + return getStorageModificationTime(item) + .then(function (mdate) { if (!request.isRunning()) { Zotero.debug("Upload request '" + request.name + "' is no longer running after getting mod time"); - return; + return false; + } + + // Check for conflict + if (Zotero.Sync.Storage.getSyncState(item.id) + != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) { + if (mdate) { + // Remote prop time + var mtime = mdate.getTime(); + + // Local file time + var fmtime = item.attachmentModificationTime; + + var same = false; + if (fmtime == mtime) { + same = true; + Zotero.debug("File mod time matches remote file -- skipping upload"); + } + // Allow floored timestamps for filesystems that don't support + // millisecond precision (e.g., HFS+) + else if (Math.floor(mtime / 1000) * 1000 == fmtime || Math.floor(fmtime / 1000) * 1000 == mtime) { + same = true; + Zotero.debug("File mod times are within one-second precision (" + fmtime + " ≅ " + mtime + ") " + + "-- skipping upload"); + } + // Allow timestamp to be exactly one hour off to get around + // time zone issues -- there may be a proper way to fix this + else if (Math.abs(fmtime - mtime) == 3600000 + // And check with one-second precision as well + || Math.abs(fmtime - Math.floor(mtime / 1000) * 1000) == 3600000 + || Math.abs(Math.floor(fmtime / 1000) * 1000 - mtime) == 3600000) { + same = true; + Zotero.debug("File mod time (" + fmtime + ") is exactly one hour off remote file (" + mtime + ") " + + "-- assuming time zone issue and skipping upload"); + } + + if (same) { + Zotero.DB.beginTransaction(); + var syncState = Zotero.Sync.Storage.getSyncState(item.id); + Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime, true); + Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC); + Zotero.DB.commitTransaction(); + return true; + } + + var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id); + if (smtime != mtime) { + var localData = { modTime: fmtime }; + var remoteData = { modTime: mtime }; + Zotero.Sync.Storage.QueueManager.addConflict( + request.name, localData, remoteData + ); + Zotero.debug("Conflict -- last synced file mod time " + + "does not match time on storage server" + + " (" + smtime + " != " + mtime + ")"); + return false; + } + } + else { + Zotero.debug("Remote file not found for item " + item.id); + } + } + + var file = Zotero.getTempDirectory(); + file.append(item.key + '.zip'); + + var fis = Components.classes["@mozilla.org/network/file-input-stream;1"] + .createInstance(Components.interfaces.nsIFileInputStream); + fis.init(file, 0x01, 0, 0); + + var bis = Components.classes["@mozilla.org/network/buffered-input-stream;1"] + .createInstance(Components.interfaces.nsIBufferedInputStream) + bis.init(fis, 64 * 1024); + + var uri = getItemURI(item); + + var ios = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + var channel = ios.newChannelFromURI(uri); + channel.QueryInterface(Components.interfaces.nsIUploadChannel); + channel.setUploadStream(bis, 'application/octet-stream', -1); + channel.QueryInterface(Components.interfaces.nsIHttpChannel); + channel.requestMethod = 'PUT'; + channel.allowPipelining = false; + + channel.setRequestHeader('Keep-Alive', '', false); + channel.setRequestHeader('Connection', '', false); + + var deferred = Q.defer(); + + var listener = new Zotero.Sync.Storage.StreamListener( + { + onProgress: function (a, b, c) { + request.onProgress(a, b, c); + }, + onStop: function (httpRequest, status, response, data) { + deferred.resolve( + onUploadComplete(httpRequest, status, response, data) + ); + }, + onCancel: function (httpRequest, status, data) { + onUploadCancel(httpRequest, status, data); + deferred.resolve(false); + }, + request: request, + item: item, + streams: [fis, bis] + } + ); + channel.notificationCallbacks = listener; + + var dispURI = uri.clone(); + if (dispURI.password) { + dispURI.password = '********'; + } + Zotero.debug("HTTP PUT of " + file.leafName + " to " + dispURI.spec); + + channel.asyncOpen(listener, null); + + return deferred.promise; + }); + } + + + function onUploadComplete(httpRequest, status, response, data) { + var request = data.request; + var item = data.item; + var url = httpRequest.name; + + Zotero.debug("Upload of attachment " + item.key + + " finished with status code " + status); + + switch (status) { + case 200: + case 201: + case 204: + break; + + case 403: + case 500: + Zotero.debug(response); + throw (Zotero.getString('sync.storage.error.fileUploadFailed') + + " " + Zotero.getString('sync.storage.error.checkFileSyncSettings')); + + case 507: + Zotero.debug(response); + throw Zotero.getString('sync.storage.error.webdav.insufficientSpace'); + + default: + Zotero.debug(response); + throw ("Unexpected file upload status " + status + + " in Zotero.Sync.Storage.WebDAV.onUploadComplete()"); + } + + return setStorageModificationTime(item) + .then(function (props) { + if (!request.isRunning()) { + Zotero.debug("Upload request '" + request.name + + "' is no longer running after getting mod time"); + return false; } Zotero.DB.beginTransaction(); @@ -360,28 +355,27 @@ Zotero.Sync.Storage.WebDAV = (function () { Components.utils.reportError(e); } - onChangesMade(); - request.finish(); + return { + localChanges: true, + remoteChanges: true + }; }); + } + + + function onUploadCancel(httpRequest, status, data) { + var request = data.request; + var item = data.item; + + Zotero.debug("Upload of attachment " + item.key + " cancelled with status code " + status); + + try { + var file = Zotero.getTempDirectory(); + file.append(item.key + '.zip'); + file.remove(false); } - - - function onUploadCancel(httpRequest, status, data) { - var request = data.request; - var item = data.item; - - Zotero.debug("Upload of attachment " + item.key + " cancelled with status code " + status); - - try { - var file = Zotero.getTempDirectory(); - file.append(item.key + '.zip'); - file.remove(false); - } - catch (e) { - Components.utils.reportError(e); - } - - request.finish(); + catch (e) { + Components.utils.reportError(e); } } @@ -649,8 +643,7 @@ Zotero.Sync.Storage.WebDAV = (function () { } } ); - - Zotero.Sync.Storage.EventManager.error(e); + throw e; } else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) == Ci.nsIWebProgressListener.STATE_IS_BROKEN) { var msg = Zotero.getString('sync.storage.error.webdav.sslConnectionError', host) + @@ -667,7 +660,7 @@ Zotero.Sync.Storage.WebDAV = (function () { } } ); - Zotero.Sync.Storage.EventManager.error(e); + throw e; } } } @@ -686,10 +679,6 @@ Zotero.Sync.Storage.WebDAV = (function () { }); obj.includeGroupItems = false; - Object.defineProperty(obj, "_enabled", { - get: function () this.includeUserFiles - }); - Object.defineProperty(obj, "_verified", { get: function () Zotero.Prefs.get("sync.storage.verified") }); @@ -755,7 +744,7 @@ Zotero.Sync.Storage.WebDAV = (function () { Object.defineProperty(obj, "rootURI", { get: function () { if (!_rootURI) { - throw new Error("Root URI not initialized"); + this._init(); } return _rootURI.clone(); } @@ -764,62 +753,16 @@ Zotero.Sync.Storage.WebDAV = (function () { Object.defineProperty(obj, "parentURI", { get: function () { if (!_parentURI) { - throw new Error("Parent URI not initialized"); + this._init(); } return _parentURI.clone(); } }); - obj._init = function (url, dir, username, password) { - if (!url) { - var msg = "WebDAV URL not provided"; - Zotero.debug(msg); - throw ({ - message: msg, - name: "Z_ERROR_NO_URL", - filename: "webdav.js", - toString: function () { return this.message; } - }); - } + obj._init = function () { + _rootURI = false; + _parentURI = false; - if (username && !password) { - var msg = "WebDAV password not provided"; - Zotero.debug(msg); - throw ({ - message: msg, - name: "Z_ERROR_NO_PASSWORD", - filename: "webdav.js", - toString: function () { return this.message; } - }); - } - - var ios = Components.classes["@mozilla.org/network/io-service;1"]. - getService(Components.interfaces.nsIIOService); - try { - var uri = ios.newURI(url, null, null); - if (username) { - uri.username = username; - uri.password = password; - } - } - catch (e) { - Zotero.debug(e); - Components.utils.reportError(e); - return false; - } - if (!uri.spec.match(/\/$/)) { - uri.spec += "/"; - } - _parentURI = uri; - - var uri = uri.clone(); - uri.spec += "zotero/"; - _rootURI = uri; - return true; - }; - - - obj._initFromPrefs = function () { var scheme = Zotero.Prefs.get('sync.storage.scheme'); switch (scheme) { case 'http': @@ -832,7 +775,14 @@ Zotero.Sync.Storage.WebDAV = (function () { var url = Zotero.Prefs.get('sync.storage.url'); if (!url) { - return false; + var msg = "WebDAV URL not provided"; + Zotero.debug(msg); + throw ({ + message: msg, + name: "Z_ERROR_NO_URL", + filename: "webdav.js", + toString: function () { return this.message; } + }); } url = scheme + '://' + url; @@ -840,7 +790,41 @@ Zotero.Sync.Storage.WebDAV = (function () { var username = this._username; var password = this._password; - return this._init(url, dir, username, password); + if (!username) { + var msg = "WebDAV username not provided"; + Zotero.debug(msg); + throw ({ + message: msg, + name: "Z_ERROR_NO_USERNAME", + filename: "webdav.js", + toString: function () { return this.message; } + }); + } + + if (!password) { + var msg = "WebDAV password not provided"; + Zotero.debug(msg); + throw ({ + message: msg, + name: "Z_ERROR_NO_PASSWORD", + filename: "webdav.js", + toString: function () { return this.message; } + }); + } + + var ios = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + var uri = ios.newURI(url, null, null); + uri.username = username; + uri.password = password; + if (!uri.spec.match(/\/$/)) { + uri.spec += "/"; + } + _parentURI = uri; + + var uri = uri.clone(); + uri.spec += "zotero/"; + _rootURI = uri; }; @@ -856,20 +840,20 @@ Zotero.Sync.Storage.WebDAV = (function () { } // Retrieve modification time from server to store locally afterwards - getStorageModificationTime(item, function (item, mdate) { - if (!request.isRunning()) { - Zotero.debug("Download request '" + request.name - + "' is no longer running after getting mod time"); - return; - } - - if (!mdate) { - Zotero.debug("Remote file not found for item " + Zotero.Items.getLibraryKeyHash(item)); - request.finish(); - return; - } - - try { + return getStorageModificationTime(item) + .then(function (mdate) { + if (!request.isRunning()) { + Zotero.debug("Download request '" + request.name + + "' is no longer running after getting mod time"); + return false; + } + + if (!mdate) { + Zotero.debug("Remote file not found for item " + Zotero.Items.getLibraryKeyHash(item)); + request.finish(); + return false; + } + var syncModTime = mdate.getTime(); // Skip download if local file exists and matches mod time @@ -883,9 +867,9 @@ Zotero.Sync.Storage.WebDAV = (function () { Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem); Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC); Zotero.DB.commitTransaction(); - onChangesMade(); - request.finish(); - return; + return { + localChanges: true + }; } var uri = getItemURI(item); @@ -895,6 +879,8 @@ Zotero.Sync.Storage.WebDAV = (function () { destFile.remove(false); } + var deferred = Q.defer(); + var listener = new Zotero.Sync.Storage.StreamListener( { onStart: function (request, data) { @@ -902,7 +888,7 @@ Zotero.Sync.Storage.WebDAV = (function () { Zotero.debug("Download request " + data.request.name + " stopped before download started -- closing channel"); request.cancel(0x804b0002); // NS_BINDING_ABORTED - return; + deferred.resolve(false); } }, onProgress: function (a, b, c) { @@ -917,7 +903,7 @@ Zotero.Sync.Storage.WebDAV = (function () { // Delete the orphaned prop file deleteStorageFiles([item.key + ".prop"]); - data.request.finish(); + deferred.resolve(false); return; } else if (status != 200) { @@ -926,26 +912,31 @@ Zotero.Sync.Storage.WebDAV = (function () { + " in Zotero.Sync.Storage.WebDAV.downloadFile()"; Zotero.debug(msg, 1); Components.utils.reportError(msg); - Zotero.Sync.Storage.EventManager.error(_defaultError); + deferred.reject(_defaultError); + return; } // Don't try to process if the request has been cancelled if (data.request.isFinished()) { Zotero.debug("Download request " + data.request.name + " is no longer running after file download"); + deferred.resolve(false); return; } Zotero.debug("Finished download of " + destFile.path); try { - Zotero.Sync.Storage.processDownload(data); - data.request.finish(); + deferred.resolve(Zotero.Sync.Storage.processDownload(data)); } catch (e) { - Zotero.Sync.Storage.EventManager.error(e); + deferred.reject(e); } }, + onCancel: function (request, status, data) { + Zotero.debug("Request cancelled"); + deferred.resolve(false); + }, request: request, item: item, compressed: true, @@ -972,146 +963,134 @@ Zotero.Sync.Storage.WebDAV = (function () { // XXX Always use when we no longer support Firefox < 18 wbp.saveURI(uri, null, null, null, null, destFile, null); } - } - catch (e) { - request.error(e); - } - }); + + return deferred.promise; + }); }; obj._uploadFile = function (request) { - Zotero.Sync.Storage.createUploadFile(request, function (data) { processUploadFile(data); }); + var deferred = Q.defer(); + Zotero.Sync.Storage.createUploadFile( + request, + function (data) { + deferred.resolve(processUploadFile(data)); + } + ); + return deferred.promise; }; - obj._getLastSyncTime = function (callback) { + obj._getLastSyncTime = function () { // Cache the credentials at the root URI var self = this; - this._cacheCredentials(function () { - try { - var uri = this.rootURI; - var successFileURI = uri.clone(); - successFileURI.spec += "lastsync"; - Zotero.HTTP.doGet(successFileURI, function (req) { - var ts = undefined; - try { - if (req.responseText) { - Zotero.debug(req.responseText); - } - Zotero.debug(req.status); - - if (req.status == 403) { - Zotero.debug("Clearing WebDAV authentication credentials", 2); - _cachedCredentials = false; - } - - if (req.status != 200 && req.status != 404) { - var msg = "Unexpected status code " + req.status + " for HEAD request " - + "in Zotero.Sync.Storage.WebDAV.getLastSyncTime()"; - Zotero.debug(msg, 1); - Components.utils.reportError(msg); - Zotero.Sync.Storage.EventManager.error(_defaultError); - } - - if (req.status == 200) { - var lastModified = req.getResponseHeader("Last-Modified"); - var date = new Date(lastModified); - Zotero.debug("Last successful storage sync was " + date); - ts = Zotero.Date.toUnixTimestamp(date); - } - else { - ts = null; - } - - callback(ts); - } - catch(e) { - Zotero.debug(e, 1); - Components.utils.reportError(e); - Zotero.Sync.Storage.EventManager.error(_defaultError); - } - }); - return; + return Q.fcall(function () { + return self._cacheCredentials(); + }) + .then(function () { + var lastSyncURI = this.rootURI; + lastSyncURI.spec += "lastsync"; + return Zotero.HTTP.promise("GET", lastSyncURI, + { debug: true, successCodes: [200, 404] }); + }) + .then(function (req) { + if (req.status == 404) { + Zotero.debug("No last WebDAV sync time"); + return null; } - catch (e) { - Zotero.debug(e); - Components.utils.reportError(e); - Zotero.Sync.Storage.EventManager.error(_defaultError); + + var lastModified = req.getResponseHeader("Last-Modified"); + var date = new Date(lastModified); + Zotero.debug("Last successful WebDAV sync was " + date); + return Zotero.Date.toUnixTimestamp(date); + }) + .fail(function (e) { + if (e instanceof Zotero.HTTP.UnexpectedStatusException) { + if (e.status == 403) { + Zotero.debug("Clearing WebDAV authentication credentials", 2); + _cachedCredentials = false; + } + else { + throw("Unexpected status code " + e.status + " getting " + + "WebDAV last sync time"); + } + + return Q.reject(e); + } + // TODO: handle browser offline exception + else { + throw (e); } }); }; - obj._setLastSyncTime = function (callback) { - try { - var uri = this.rootURI; - var successFileURI = uri.clone(); - successFileURI.spec += "lastsync"; - - Zotero.HTTP.WebDAV.doPut(successFileURI, " ", function (req) { - Zotero.debug(req.responseText); - Zotero.debug(req.status); - - switch (req.status) { - case 200: - case 201: - case 204: - getLastSyncTime(function (ts) { - if (ts) { - var sql = "REPLACE INTO version VALUES ('storage_webdav', ?)"; - Zotero.DB.query(sql, { int: ts }); - } - if (callback) { - callback(); - } - }); - return; - } - + obj._setLastSyncTime = function (libraryID, localLastSyncTime) { + if (libraryID) { + throw new Error("libraryID must be 0"); + } + + // DEBUG: is this necessary for WebDAV? + if (localLastSyncTime) { + var sql = "REPLACE INTO version VALUES (?, ?)"; + Zotero.DB.query( + sql, ['storage_webdav_' + libraryID, { int: localLastSyncTime }] + ); + return; + } + + var uri = this.rootURI; + var successFileURI = uri.clone(); + successFileURI.spec += "lastsync"; + + var self = this; + + return Zotero.HTTP.promise("PUT", successFileURI, " ", + { debug: true, successCodes: [200, 201, 204] }) + .then(function () { + return self._getLastSyncTime() + .then(function (ts) { + if (ts) { + var sql = "REPLACE INTO version VALUES (?, ?)"; + Zotero.DB.query( + sql, ['storage_webdav_' + libraryID, { int: ts }] + ); + } + }); + }) + .fail(function (e) { var msg = "Unexpected error code " + req.status + " uploading storage success file"; Zotero.debug(msg, 2); Components.utils.reportError(msg); - if (callback) { - callback(); - } + throw _defaultError; }); - } - catch (e) { - Zotero.debug(e); - Components.utils.reportError(e); - if (callback) { - callback(); - } - return; - } }; - obj._cacheCredentials = function (callback) { + obj._cacheCredentials = function () { if (_cachedCredentials) { Zotero.debug("Credentials are already cached"); - setTimeout(function () { - callback(); - }, 0); - return false; + return; } - Zotero.HTTP.doOptions(this.rootURI, function (req) { - checkResponse(req); - - if (req.status != 200) { - var msg = "Unexpected status code " + req.status + " for OPTIONS request " - + "in Zotero.Sync.Storage.WebDAV.getLastSyncTime()"; - Zotero.debug(msg, 1); - Components.utils.reportError(msg); - Zotero.Sync.Storage.EventManager.error(_defaultErrorRestart); - } - Zotero.debug("Credentials are cached"); - _cachedCredentials = true; - callback(); - }); - return true; + return Zotero.HTTP.promise("OPTIONS", this.rootURI) + .then(function (req) { + // TODO: promisify + checkResponse(req); + + Zotero.debug("Credentials are cached"); + _cachedCredentials = true; + }) + .fail(function (e) { + if (e instanceof Zotero.HTTP.UnexpectedStatusException) { + var msg = "Unexpected status code " + e.status + " " + + "for OPTIONS request caching WebDAV credentials"; + Zotero.debug(msg, 1); + Components.utils.reportError(msg); + throw new Error(_defaultErrorRestart); + } + throw e; + }); }; @@ -1120,9 +1099,10 @@ Zotero.Sync.Storage.WebDAV = (function () { * @param {Object} errorCallbacks */ obj._checkServer = function (callback) { - this._initFromPrefs(); - try { + // Clear URIs + this.init(); + var parentURI = this.parentURI; var uri = this.rootURI; } @@ -1132,6 +1112,10 @@ Zotero.Sync.Storage.WebDAV = (function () { callback(null, Zotero.Sync.Storage.ERROR_NO_URL); return; + case 'Z_ERROR_NO_USERNAME': + callback(null, Zotero.Sync.Storage.ERROR_NO_USERNAME); + return; + case 'Z_ERROR_NO_PASSWORD': callback(null, Zotero.Sync.Storage.ERROR_NO_PASSWORD); return; @@ -1399,6 +1383,10 @@ Zotero.Sync.Storage.WebDAV = (function () { var errorMessage = Zotero.getString('sync.storage.error.webdav.enterURL'); break; + case Zotero.Sync.Storage.ERROR_NO_USERNAME: + var errorMessage = Zotero.getString('sync.error.usernameNotSet'); + break; + case Zotero.Sync.Storage.ERROR_NO_PASSWORD: var errorMessage = Zotero.getString('sync.error.enterPassword'); break; @@ -1525,7 +1513,7 @@ Zotero.Sync.Storage.WebDAV = (function () { */ obj._purgeDeletedStorageFiles = function (callback) { if (!this._active) { - return; + return false; } Zotero.debug("Purging deleted storage files"); @@ -1535,10 +1523,11 @@ Zotero.Sync.Storage.WebDAV = (function () { if (callback) { callback(); } - Zotero.Sync.Storage.EventManager.skip(); - return; + return false; } + // TODO: promisify + // Add .zip extension var files = files.map(function (file) file + ".zip"); @@ -1582,16 +1571,14 @@ Zotero.Sync.Storage.WebDAV = (function () { const daysBeforeSyncTime = 1; if (!this._active) { - Zotero.Sync.Storage.EventManager.skip(); - return; + return false; } // If recently purged, skip var lastpurge = Zotero.Prefs.get('lastWebDAVOrphanPurge'); var days = 10; if (lastpurge && new Date(lastpurge * 1000) > (new Date() - (1000 * 60 * 60 * 24 * days))) { - Zotero.Sync.Storage.EventManager.skip(); - return; + return false; } Zotero.debug("Purging orphaned storage files"); diff --git a/chrome/content/zotero/xpcom/storage/zfs.js b/chrome/content/zotero/xpcom/storage/zfs.js index 7f07cb96f..bbf76d1d5 100644 --- a/chrome/content/zotero/xpcom/storage/zfs.js +++ b/chrome/content/zotero/xpcom/storage/zfs.js @@ -28,7 +28,6 @@ Zotero.Sync.Storage.ZFS = (function () { var _rootURI; var _userURI; var _cachedCredentials = false; - var _lastSyncTime = null; /** * Get file metadata on storage server @@ -36,53 +35,55 @@ Zotero.Sync.Storage.ZFS = (function () { * @param {Zotero.Item} item * @param {Function} callback Callback f(item, etag) */ - function getStorageFileInfo(item, callback) { - var uri = getItemInfoURI(item); + function getStorageFileInfo(item) { + var funcName = "Zotero.Sync.Storage.ZFS.getStorageFileInfo()"; - Zotero.HTTP.doGet(uri, function (req) { - var funcName = "Zotero.Sync.Storage.ZFS.getStorageFileInfo()"; - - if (req.status == 404) { - callback(item, false); - return; - } - else if (req.status != 200) { - var msg = "Unexpected status code " + req.status + " in " + funcName - + " (" + Zotero.Items.getLibraryKeyHash(item) + ")"; - Zotero.debug(msg, 1); - Zotero.debug(req.responseText); - Components.utils.reportError(msg); - Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError); - } - - var info = {}; - info.hash = req.getResponseHeader('ETag'); - if (!info.hash) { - var msg = "Hash not found in info response in " + funcName - + " (" + Zotero.Items.getLibraryKeyHash(item) + ")"; - Zotero.debug(msg, 1); - Zotero.debug(req.responseText); - Components.utils.reportError(msg); - try { - Zotero.debug(req.getAllResponseHeaders()); + return Zotero.HTTP.promise("GET", getItemInfoURI(item), { successCodes: [200, 404] }) + .then(function (req) { + if (req.status == 404) { + return false; } - catch (e) { - Zotero.debug("Response headers unavailable"); + + var info = {}; + info.hash = req.getResponseHeader('ETag'); + if (!info.hash) { + var msg = "Hash not found in info response in " + funcName + + " (" + Zotero.Items.getLibraryKeyHash(item) + ")"; + Zotero.debug(msg, 1); + Zotero.debug(req.responseText); + Components.utils.reportError(msg); + try { + Zotero.debug(req.getAllResponseHeaders()); + } + catch (e) { + Zotero.debug("Response headers unavailable"); + } + // TODO: localize? + var msg = "A file sync error occurred. Please restart " + Zotero.appName + " and/or your computer and try syncing again.\n\n" + + "If the error persists, there may be a problem with either your computer or your network: security software, proxy server, VPN, etc. " + + "Try disabling any security/firewall software you're using or, if this is a laptop, try from a different network."; + throw msg; } - // TODO: localize? - var msg = "A file sync error occurred. Please restart " + Zotero.appName + " and/or your computer and try syncing again.\n\n" - + "If the error persists, there may be a problem with either your computer or your network: security software, proxy server, VPN, etc. " - + "Try disabling any security/firewall software you're using or, if this is a laptop, try from a different network."; - Zotero.Sync.Storage.EventManager.error(msg); - } - info.filename = req.getResponseHeader('X-Zotero-Filename'); - var mtime = req.getResponseHeader('X-Zotero-Modification-Time'); - info.mtime = parseInt(mtime); - info.compressed = req.getResponseHeader('X-Zotero-Compressed') == 'Yes'; - Zotero.debug(info); - - callback(item, info); - }); + info.filename = req.getResponseHeader('X-Zotero-Filename'); + var mtime = req.getResponseHeader('X-Zotero-Modification-Time'); + info.mtime = parseInt(mtime); + info.compressed = req.getResponseHeader('X-Zotero-Compressed') == 'Yes'; + Zotero.debug(info); + + return info; + }) + .fail(function (e) { + if (e instanceof Zotero.HTTP.UnexpectedStatusException) { + var msg = "Unexpected status code " + e.xmlhttp.status + + " getting storage file info"; + Zotero.debug(msg, 1); + Zotero.debug(e.xmlhttp.responseText); + Components.utils.reportError(msg); + throw new Error(Zotero.Sync.Storage.defaultError); + } + + throw e; + }); } @@ -101,14 +102,14 @@ Zotero.Sync.Storage.ZFS = (function () { var request = data.request; var item = Zotero.Sync.Storage.getItemFromRequestName(request.name); - getStorageFileInfo(item, function (item, info) { - if (request.isFinished()) { - Zotero.debug("Upload request '" + request.name - + "' is no longer running after getting file info"); - return; - } - - try { + return getStorageFileInfo(item) + .then(function (info) { + if (request.isFinished()) { + Zotero.debug("Upload request '" + request.name + + "' is no longer running after getting file info"); + return false; + } + // Check for conflict if (Zotero.Sync.Storage.getSyncState(item.id) != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) { @@ -156,23 +157,25 @@ Zotero.Sync.Storage.ZFS = (function () { Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime); Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC); Zotero.DB.commitTransaction(); - Zotero.Sync.Storage.EventManager.changesMade(); - request.finish(); - return; + return { + localChanges: true, + remoteChanges: false + }; } var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id); if (!useLocal && smtime != mtime) { - var localData = { modTime: fmtime }; - var remoteData = { modTime: mtime }; - Zotero.Sync.Storage.QueueManager.addConflict( - request.name, localData, remoteData - ); Zotero.debug("Conflict -- last synced file mod time " + "does not match time on storage server" + " (" + smtime + " != " + mtime + ")"); - request.finish(); - return; + return { + localChanges: false, + remoteChanges: false, + conflict: { + local: { modTime: fmtime }, + remote: { modTime: mtime } + } + }; } } else { @@ -180,26 +183,20 @@ Zotero.Sync.Storage.ZFS = (function () { } } - getFileUploadParameters( + return getFileUploadParameters( item, function (item, target, uploadKey, params) { - try { - postFile(request, item, target, uploadKey, params); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } + return postFile(request, item, target, uploadKey, params); }, function () { updateItemFileInfo(item); - request.finish(); + return { + localChanges: true, + remoteChanges: false + }; } ); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } - }); + }); } @@ -212,6 +209,8 @@ Zotero.Sync.Storage.ZFS = (function () { * on server and uploading isn't necessary */ function getFileUploadParameters(item, uploadCallback, existsCallback) { + var funcName = "Zotero.Sync.Storage.ZFS.getFileUploadParameters()"; + var uri = getItemURI(item); if (Zotero.Attachments.getNumFiles(item) > 1) { @@ -236,145 +235,137 @@ Zotero.Sync.Storage.ZFS = (function () { body += "&zip=1"; } - Zotero.HTTP.doPost(uri, body, function (req) { - var funcName = "Zotero.Sync.Storage.ZFS.getFileUploadParameters()"; - - if (req.status == 413) { - var retry = req.getResponseHeader('Retry-After'); - if (retry) { - var minutes = Math.round(retry / 60); - // TODO: localize - var e = new Zotero.Error( - "You have too many queued uploads. " - + "Please try again in " + minutes + " minutes.", - "ZFS_UPLOAD_QUEUE_LIMIT" - ); - Zotero.Sync.Storage.EventManager.error(e); + return Zotero.HTTP.promise("POST", uri, { body: body, debug: true }) + .then(function (req) { + try { + // Strip XML declaration and convert to E4X + var xml = new XML(Zotero.Utilities.trim(req.responseText.replace(/<\?xml.*\?>/, ''))); + } + catch (e) { + throw new Error("Invalid response retrieving file upload parameters"); } - var text, buttonText = null, buttonCallback; - - // Group file - if (item.libraryID) { - var group = Zotero.Groups.getByLibraryID(item.libraryID); - text = Zotero.getString('sync.storage.error.zfs.groupQuotaReached1', group.name) + "\n\n" - + Zotero.getString('sync.storage.error.zfs.groupQuotaReached2'); + if (xml.name() != 'upload' && xml.name() != 'exists') { + throw new Error("Invalid response retrieving file upload parameters"); } - // Personal file - else { - text = Zotero.getString('sync.storage.error.zfs.personalQuotaReached1') + "\n\n" - + Zotero.getString('sync.storage.error.zfs.personalQuotaReached2'); - buttonText = Zotero.getString('sync.storage.openAccountSettings'); - buttonCallback = function () { - var url = "https://www.zotero.org/settings/storage"; + + // File was already available, so uploading isn't required + if (xml.name() == 'exists') { + existsCallback(); + return false; + } + + var url = xml.url.toString(); + var uploadKey = xml.key.toString(); + var params = {}, p = ''; + for each(var param in xml.params.children()) { + params[param.name()] = param.toString(); + } + Zotero.debug(params); + return uploadCallback(item, url, uploadKey, params); + }) + .fail(function (e) { + if (e instanceof Zotero.HTTP.UnexpectedStatusException) { + if (e.status == 413) { + var retry = e.xmlhttp.getResponseHeader('Retry-After'); + if (retry) { + var minutes = Math.round(retry / 60); + // TODO: localize + var e = new Zotero.Error( + "You have too many queued uploads. " + + "Please try again in " + minutes + " minutes.", + "ZFS_UPLOAD_QUEUE_LIMIT" + ); + throw e; + } - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - var win = wm.getMostRecentWindow("navigator:browser"); - var browser = win.getBrowser(); - browser.selectedTab = browser.addTab(url); + var text, buttonText = null, buttonCallback; + + // Group file + if (item.libraryID) { + var group = Zotero.Groups.getByLibraryID(item.libraryID); + text = Zotero.getString('sync.storage.error.zfs.groupQuotaReached1', group.name) + "\n\n" + + Zotero.getString('sync.storage.error.zfs.groupQuotaReached2'); + } + // Personal file + else { + text = Zotero.getString('sync.storage.error.zfs.personalQuotaReached1') + "\n\n" + + Zotero.getString('sync.storage.error.zfs.personalQuotaReached2'); + buttonText = Zotero.getString('sync.storage.openAccountSettings'); + buttonCallback = function () { + var url = "https://www.zotero.org/settings/storage"; + + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var win = wm.getMostRecentWindow("navigator:browser"); + var browser = win.getBrowser(); + browser.selectedTab = browser.addTab(url); + } + } + + // TODO: localize + text += "\n\n" + filename + " (" + Math.round(file.fileSize / 1024) + "KB)"; + + var e = new Zotero.Error( + "The file '" + filename + "' would exceed your Zotero File Storage quota", + "ZFS_OVER_QUOTA", + { + dialogText: text, + dialogButtonText: buttonText, + dialogButtonCallback: buttonCallback + } + ); + Zotero.debug(e, 2); + Components.utils.reportError(e); + // Stop uploads from this library, log warning, and continue + Zotero.Sync.Storage.QueueManager.get('upload', item.libraryID).stop(); + Zotero.Sync.Storage.EventLog.warning(e, item.libraryID); + return false; } + else if (e.status == 403) { + var groupID = Zotero.Groups.getGroupIDFromLibraryID(item.libraryID); + var e = new Zotero.Error( + "File editing denied for group", + "ZFS_FILE_EDITING_DENIED", + { + groupID: groupID + } + ); + throw e; + } + else if (e.status == 404) { + Components.utils.reportError("Unexpected status code 404 in " + funcName + + " (" + Zotero.Items.getLibraryKeyHash(item) + ")"); + if (Zotero.Prefs.get('sync.debugNoAutoResetClient')) { + Components.utils.reportError("Skipping automatic client reset due to debug pref"); + return; + } + if (!Zotero.Sync.Server.canAutoResetClient) { + Components.utils.reportError("Client has already been auto-reset -- manual sync required"); + return; + } + Zotero.Sync.Server.resetClient(); + Zotero.Sync.Server.canAutoResetClient = false; + throw new Error(Zotero.Sync.Storage.defaultError); + } + + var msg = "Unexpected status code " + e.status + " in " + funcName + + " (" + Zotero.Items.getLibraryKeyHash(item) + ")"; + Zotero.debug(msg, 1); + Zotero.debug(e.xmlhttp.getAllResponseHeaders()); + Components.utils.reportError(msg); + throw new Error(Zotero.Sync.Storage.defaultError); } - // TODO: localize - text += "\n\n" + filename + " (" + Math.round(file.fileSize / 1024) + "KB)"; - - Zotero.debug(req.responseText); - - var e = new Zotero.Error( - "The file '" + filename + "' would exceed your Zotero File Storage quota", - "ZFS_OVER_QUOTA", - { - dialogText: text, - dialogButtonText: buttonText, - dialogButtonCallback: buttonCallback - } - ); - Zotero.debug(e, 2); - Components.utils.reportError(e); - // Stop uploads, log warning, and continue - Zotero.Sync.Storage.QueueManager.get('upload').stop(); - Zotero.Sync.Storage.EventManager.warning(e); - Zotero.Sync.Storage.EventManager.success(); - return; - } - else if (req.status == 403) { - Zotero.debug(req.responseText); - - var groupID = Zotero.Groups.getGroupIDFromLibraryID(item.libraryID); - var e = new Zotero.Error( - "File editing denied for group", - "ZFS_FILE_EDITING_DENIED", - { - groupID: groupID - } - ); - Zotero.Sync.Storage.EventManager.error(e); - } - else if (req.status == 404) { - Components.utils.reportError("Unexpected status code 404 in " + funcName - + " (" + Zotero.Items.getLibraryKeyHash(item) + ")"); - if (Zotero.Prefs.get('sync.debugNoAutoResetClient')) { - Components.utils.reportError("Skipping automatic client reset due to debug pref"); - return; - } - if (!Zotero.Sync.Server.canAutoResetClient) { - Components.utils.reportError("Client has already been auto-reset -- manual sync required"); - return; - } - Zotero.Sync.Server.resetClient(); - Zotero.Sync.Server.canAutoResetClient = false; - Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError); - } - else if (req.status != 200) { - var msg = "Unexpected status code " + req.status + " in " + funcName - + " (" + Zotero.Items.getLibraryKeyHash(item) + ")"; - Zotero.debug(msg, 1); - Zotero.debug(req.responseText); - Zotero.debug(req.getAllResponseHeaders()); - Components.utils.reportError(msg); - Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError); - } - - Zotero.debug(req.responseText); - - try { - // Strip XML declaration and convert to E4X - var xml = new XML(Zotero.Utilities.trim(req.responseText.replace(/<\?xml.*\?>/, ''))); - } - catch (e) { - Zotero.Sync.Storage.EventManager.error( - "Invalid response retrieving file upload parameters" - ); - } - - if (xml.name() != 'upload' && xml.name() != 'exists') { - Zotero.Sync.Storage.EventManager.error( - "Invalid response retrieving file upload parameters" - ); - } - // File was already available, so uploading isn't required - if (xml.name() == 'exists') { - existsCallback(); - return; - } - - var url = xml.url.toString(); - var uploadKey = xml.key.toString(); - var params = {}, p = ''; - for each(var param in xml.params.children()) { - params[param.name()] = param.toString(); - } - Zotero.debug(params); - uploadCallback(item, url, uploadKey, params); - }); + throw e; + }); } function postFile(request, item, url, uploadKey, params) { if (request.isFinished()) { Zotero.debug("Upload request " + request.name + " is no longer running after getting upload parameters"); - return; + return false; } var file = getUploadFile(item); @@ -458,13 +449,22 @@ Zotero.Sync.Storage.ZFS = (function () { request.setChannel(channel); + var deferred = Q.defer(); + var listener = new Zotero.Sync.Storage.StreamListener( { onProgress: function (a, b, c) { request.onProgress(a, b, c); }, - onStop: function (httpRequest, status, response, data) { onUploadComplete(httpRequest, status, response, data); }, - onCancel: function (httpRequest, status, data) { onUploadCancel(httpRequest, status, data); }, + onStop: function (httpRequest, status, response, data) { + deferred.resolve( + onUploadComplete(httpRequest, status, response, data) + ); + }, + onCancel: function (httpRequest, status, data) { + onUploadCancel(httpRequest, status, data) + deferred.resolve(false); + }, request: request, item: item, uploadKey: uploadKey, @@ -480,6 +480,8 @@ Zotero.Sync.Storage.ZFS = (function () { Zotero.debug("HTTP POST of " + file.leafName + " to " + dispURI.spec); channel.asyncOpen(listener, null); + + return deferred.promise; } @@ -498,9 +500,7 @@ Zotero.Sync.Storage.ZFS = (function () { break; case 500: - Zotero.Sync.Storage.EventManager.error( - "File upload failed. Please try again." - ); + throw new Error("File upload failed. Please try again."); default: var msg = "Unexpected file upload status " + status @@ -509,30 +509,31 @@ Zotero.Sync.Storage.ZFS = (function () { Zotero.debug(msg, 1); Components.utils.reportError(msg); Components.utils.reportError(response); - Components.utils.reportError(msg); - Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError); + throw new Error(Zotero.Sync.Storage.defaultError); } var uri = getItemURI(item); var body = "update=" + uploadKey + "&mtime=" + item.attachmentModificationTime; // Register upload on server - Zotero.HTTP.doPost(uri, body, function (req) { - if (req.status != 204) { - var msg = "Unexpected file registration status " + req.status - + " in Zotero.Sync.Storage.ZFS.onUploadComplete()" + return Zotero.HTTP.promise("POST", uri, { body: body, successCodes: [204] }) + .then(function (req) { + updateItemFileInfo(item); + return { + localChanges: true, + remoteChanges: true + }; + }) + .fail(function (e) { + var msg = "Unexpected file registration status " + e.status + " (" + Zotero.Items.getLibraryKeyHash(item) + ")"; Zotero.debug(msg, 1); - Zotero.debug(req.responseText); - Zotero.debug(req.getAllResponseHeaders()); + Zotero.debug(e.xmlhttp.responseText); + Zotero.debug(e.xmlhttp.getAllResponseHeaders()); Components.utils.reportError(msg); - Components.utils.reportError(req.responseText); - Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError); - } - - updateItemFileInfo(item); - request.finish(); - }); + Components.utils.reportError(e.xmlhttp.responseText); + throw new Error(Zotero.Sync.Storage.defaultError); + }); } @@ -563,8 +564,6 @@ Zotero.Sync.Storage.ZFS = (function () { catch (e) { Components.utils.reportError(e); } - - Zotero.Sync.Storage.EventManager.changesMade(); } @@ -584,8 +583,6 @@ Zotero.Sync.Storage.ZFS = (function () { catch (e) { Components.utils.reportError(e); } - - request.finish(); } @@ -649,16 +646,12 @@ Zotero.Sync.Storage.ZFS = (function () { } }); - Object.defineProperty(obj, "_enabled", { - get: function () this.includeUserFiles || this.includeGroupFiles - }); - obj._verified = true; Object.defineProperty(obj, "rootURI", { get: function () { if (!_rootURI) { - throw ("Root URI not initialized in Zotero.Sync.Storage.ZFS.rootURI"); + this._init(); } return _rootURI.clone(); } @@ -667,7 +660,7 @@ Zotero.Sync.Storage.ZFS = (function () { Object.defineProperty(obj, "userURI", { get: function () { if (!_userURI) { - throw ("User URI not initialized in Zotero.Sync.Storage.ZFS.userURI"); + this._init(); } return _userURI.clone(); } @@ -675,35 +668,23 @@ Zotero.Sync.Storage.ZFS = (function () { obj._init = function (url, username, password) { + _rootURI = false; + _userURI = false; + + var url = ZOTERO_CONFIG.API_URL; + var username = Zotero.Sync.Server.username; + var password = Zotero.Sync.Server.password; + var ios = Components.classes["@mozilla.org/network/io-service;1"]. getService(Components.interfaces.nsIIOService); - try { - var uri = ios.newURI(url, null, null); - if (username) { - uri.username = username; - uri.password = password; - } - } - catch (e) { - Zotero.debug(e, 1); - Components.utils.reportError(e); - return false; - } + var uri = ios.newURI(url, null, null); + uri.username = username; + uri.password = password; _rootURI = uri; uri = uri.clone(); uri.spec += 'users/' + Zotero.userID + '/'; _userURI = uri; - - return true; - }; - - - obj._initFromPrefs = function () { - var url = ZOTERO_CONFIG.API_URL; - var username = Zotero.Sync.Server.username; - var password = Zotero.Sync.Server.password; - return this._init(url, username, password); }; @@ -719,20 +700,19 @@ Zotero.Sync.Storage.ZFS = (function () { } // Retrieve file info from server to store locally afterwards - getStorageFileInfo(item, function (item, info) { - if (!request.isRunning()) { - Zotero.debug("Download request '" + request.name - + "' is no longer running after getting remote file info"); - return; - } - - if (!info) { - Zotero.debug("Remote file not found for item " + item.libraryID + "/" + item.key); - request.finish(); - return; - } - - try { + return getStorageFileInfo(item) + .then(function (info) { + if (!request.isRunning()) { + Zotero.debug("Download request '" + request.name + + "' is no longer running after getting remote file info"); + return false; + } + + if (!info) { + Zotero.debug("Remote file not found for item " + item.libraryID + "/" + item.key); + return false; + } + var syncModTime = info.mtime; var syncHash = info.hash; @@ -749,9 +729,10 @@ Zotero.Sync.Storage.ZFS = (function () { Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem); Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC); Zotero.DB.commitTransaction(); - Zotero.Sync.Storage.EventManager.changesMade(); - request.finish(); - return; + return { + localChanges: true, + remoteChanges: false + }; } // If not compressed, check hash, in case only timestamp changed else if (!info.compressed && item.attachmentHash == syncHash) { @@ -767,9 +748,10 @@ Zotero.Sync.Storage.ZFS = (function () { Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem); Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC); Zotero.DB.commitTransaction(); - Zotero.Sync.Storage.EventManager.changesMade(); - request.finish(); - return; + return { + localChanges: true, + remoteChanges: false + }; } } @@ -799,6 +781,8 @@ Zotero.Sync.Storage.ZFS = (function () { Zotero.File.checkFileAccessError(e, destFile, 'create'); } + var deferred = Q.defer(); + var listener = new Zotero.Sync.Storage.StreamListener( { onStart: function (request, data) { @@ -806,7 +790,7 @@ Zotero.Sync.Storage.ZFS = (function () { Zotero.debug("Download request " + data.request.name + " stopped before download started -- closing channel"); request.cancel(0x804b0002); // NS_BINDING_ABORTED - return; + deferred.resolve(false); } }, onProgress: function (a, b, c) { @@ -819,26 +803,31 @@ Zotero.Sync.Storage.ZFS = (function () { + " in Zotero.Sync.Storage.ZFS.downloadFile()"; Zotero.debug(msg, 1); Components.utils.reportError(msg); - Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError); + deferred.reject(Zotero.Sync.Storage.defaultError); + return; } // Don't try to process if the request has been cancelled if (data.request.isFinished()) { Zotero.debug("Download request " + data.request.name + " is no longer running after file download", 2); + deferred.resolve(false); return; } Zotero.debug("Finished download of " + destFile.path); try { - Zotero.Sync.Storage.processDownload(data); - data.request.finish(); + deferred.resolve(Zotero.Sync.Storage.processDownload(data)); } catch (e) { - Zotero.Sync.Storage.EventManager.error(e); + deferred.reject(e); } }, + onCancel: function (request, status, data) { + Zotero.debug("Request cancelled"); + deferred.resolve(false); + }, request: request, item: item, compressed: info.compressed, @@ -868,151 +857,160 @@ Zotero.Sync.Storage.ZFS = (function () { // XXX Always use when we no longer support Firefox < 18 wbp.saveURI(uri, null, null, null, null, destFile, null); } - } - catch (e) { - Zotero.Sync.Storage.EventManager.error(e); - } - }); + + return deferred.promise; + }); }; obj._uploadFile = function (request) { var item = Zotero.Sync.Storage.getItemFromRequestName(request.name); if (Zotero.Attachments.getNumFiles(item) > 1) { - Zotero.Sync.Storage.createUploadFile(request, function (data) { processUploadFile(data); }); + var deferred = Q.defer(); + Zotero.Sync.Storage.createUploadFile( + request, + function (data) { + deferred.resolve(processUploadFile(data)); + } + ); + return deferred.promise; } else { - processUploadFile({ request: request }); + return processUploadFile({ request: request }); } }; - obj._getLastSyncTime = function (callback) { - var uri = this.userURI; - var successFileURI = uri.clone(); - successFileURI.spec += "laststoragesync?auth=1"; + /** + * @return {Promise} A promise for the last sync time + */ + obj._getLastSyncTime = function (libraryID) { + var lastSyncURI = this._getLastSyncURI(libraryID); - // Cache the credentials at the root var self = this; - this._cacheCredentials(function () { - Zotero.HTTP.doGet(successFileURI, function (req) { - if (req.responseText) { - Zotero.debug(req.responseText); - } - Zotero.debug(req.status); - - if (req.status == 401 || req.status == 403) { + return Q.fcall(function () { + // Cache the credentials at the root + return self._cacheCredentials(); + }) + .then(function () { + return Zotero.HTTP.promise("GET", lastSyncURI, + { debug: true, successCodes: [200, 404] }); + }) + .then(function (req) { + // Not yet synced + if (req.status == 404) { + Zotero.debug("No last sync time for library " + libraryID); + return null; + } + + var ts = req.responseText; + var date = new Date(ts * 1000); + Zotero.debug("Last successful ZFS sync for library " + + libraryID + " was " + date); + return ts; + }) + .fail(function (e) { + if (e instanceof Zotero.HTTP.UnexpectedStatusException) { + if (e.status == 401 || e.status == 403) { Zotero.debug("Clearing ZFS authentication credentials", 2); _cachedCredentials = false; } - if (req.status != 200 && req.status != 404) { - Zotero.Sync.Storage.EventManager.error( - "Unexpected status code " + req.status + " getting " - + "last file sync time" - ); - } - - if (req.status == 200) { - var ts = req.responseText; - var date = new Date(ts * 1000); - Zotero.debug("Last successful storage sync was " + date); - _lastSyncTime = ts; - } - else { - var ts = null; - _lastSyncTime = null; - } - callback(ts); - }); + return Q.reject(e); + } + // TODO: handle browser offline exception + else { + throw e; + } }); }; - obj._setLastSyncTime = function (callback, useLastSyncTime) { - if (useLastSyncTime) { - if (!_lastSyncTime) { - if (callback) { - callback(); - } - return; - } - - var sql = "REPLACE INTO version VALUES ('storage_zfs', ?)"; - Zotero.DB.query(sql, { int: _lastSyncTime }); - - Zotero.debug("Clearing ZFS authentication credentials", 2); - _lastSyncTime = null; - _cachedCredentials = false; - - if (callback) { - callback(); - } - + obj._setLastSyncTime = function (libraryID, localLastSyncTime) { + if (localLastSyncTime) { + var sql = "REPLACE INTO version VALUES (?, ?)"; + Zotero.DB.query( + sql, ['storage_zfs_' + libraryID, { int: localLastSyncTime }] + ); return; } - _lastSyncTime = null; - var uri = this.userURI; - var successFileURI = uri.clone(); - successFileURI.spec += "laststoragesync?auth=1"; + var lastSyncURI = this._getLastSyncURI(libraryID); - Zotero.HTTP.doPost(successFileURI, "", function (req) { - Zotero.debug(req.responseText); - Zotero.debug(req.status); - - if (req.status != 200) { - var msg = "Unexpected status code " + req.status + " setting last file sync time"; + return Zotero.HTTP.promise("POST", lastSyncURI, { debug: true }) + .then(function (req) { + var ts = req.responseText; + + var sql = "REPLACE INTO version VALUES (?, ?)"; + Zotero.DB.query( + sql, ['storage_zfs_' + libraryID, { int: ts }] + ); + }) + .fail(function (e) { + var msg = "Unexpected status code " + e.xmlhttp.status + + " setting last file sync time"; Zotero.debug(msg, 1); Components.utils.reportError(msg); - Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError); - } - - var ts = req.responseText; - - var sql = "REPLACE INTO version VALUES ('storage_zfs', ?)"; - Zotero.DB.query(sql, { int: ts }); - - Zotero.debug("Clearing ZFS authentication credentials", 2); - _cachedCredentials = false; - - if (callback) { - callback(); - } - }); + throw new Error(Zotero.Sync.Storage.defaultError); + }); }; - obj._cacheCredentials = function (callback) { + obj._getLastSyncURI = function (libraryID) { + if (libraryID === 0) { + var lastSyncURI = this.userURI; + } + else if (libraryID) { + var ios = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + var uri = ios.newURI(Zotero.URI.getLibraryURI(libraryID), null, null); + var path = uri.path; + // We don't want the user URI, but it already has the right domain + // and credentials, so just start with that and replace the path + var lastSyncURI = this.userURI; + lastSyncURI.path = path + "/"; + } + else { + throw new Error("libraryID not specified"); + } + lastSyncURI.spec += "laststoragesync"; + return lastSyncURI; + } + + + obj._cacheCredentials = function () { if (_cachedCredentials) { Zotero.debug("Credentials are already cached"); - setTimeout(function () { - callback(); - }, 0); - return false; + return; } var uri = this.rootURI; // TODO: move to root uri uri.spec += "?auth=1"; - Zotero.HTTP.doGet(uri, function (req) { - if (req.status == 401) { - // TODO: localize - var msg = "File sync login failed\n\nCheck your username and password in the Sync pane of the Zotero preferences."; - Zotero.Sync.Storage.EventManager.error(msg); - } - else if (req.status != 200) { - var msg = "Unexpected status code " + req.status + " caching " - + "authentication credentials in Zotero.Sync.Storage.ZFS.cacheCredentials()"; - Zotero.debug(msg, 1); - Components.utils.reportError(msg); - Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultErrorRestart); - } - Zotero.debug("Credentials are cached"); - _cachedCredentials = true; - callback(); - }); - return true; + + return Zotero.HTTP.promise("GET", uri). + then(function (req) { + Zotero.debug("Credentials are cached"); + _cachedCredentials = true; + }) + .fail(function (e) { + if (e instanceof Zotero.HTTP.UnexpectedStatusException) { + if (e.status == 401) { + var msg = "File sync login failed\n\n" + + "Check your username and password in the Sync " + + "pane of the Zotero preferences."; + throw (msg); + } + + var msg = "Unexpected status code " + e.status + " " + + "caching ZFS credentials"; + Zotero.debug(msg, 1); + throw (msg); + } + else { + throw (e); + } + }); }; @@ -1022,17 +1020,17 @@ Zotero.Sync.Storage.ZFS = (function () { obj._purgeDeletedStorageFiles = function (callback) { // If we don't have a user id we've never synced and don't need to bother if (!Zotero.userID) { - Zotero.Sync.Storage.EventManager.skip(); - return; + return false; } var sql = "SELECT value FROM settings WHERE setting=? AND key=?"; var values = Zotero.DB.columnQuery(sql, ['storage', 'zfsPurge']); if (!values) { - Zotero.Sync.Storage.EventManager.skip(); - return; + return false; } + // TODO: promisify + Zotero.debug("Unlinking synced files on ZFS"); var uri = this.userURI; @@ -1049,9 +1047,8 @@ Zotero.Sync.Storage.ZFS = (function () { break; default: - Zotero.Sync.Storage.EventManager.error( - "Invalid zfsPurge value '" + value + "' in ZFS purgeDeletedStorageFiles()" - ); + throw "Invalid zfsPurge value '" + value + + "' in ZFS purgeDeletedStorageFiles()"; } } uri.spec = uri.spec.substr(0, uri.spec.length - 1); @@ -1061,9 +1058,7 @@ Zotero.Sync.Storage.ZFS = (function () { if (callback) { callback(false); } - Zotero.Sync.Storage.EventManager.error( - "Unexpected status code " + xmlhttp.status + " purging ZFS files" - ); + throw "Unexpected status code " + xmlhttp.status + " purging ZFS files"; } var sql = "DELETE FROM settings WHERE setting=? AND key=?"; @@ -1072,8 +1067,6 @@ Zotero.Sync.Storage.ZFS = (function () { if (callback) { callback(true); } - - Zotero.Sync.Storage.EventManager.success(); }); }; diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js index eaab772cf..863e8b383 100644 --- a/chrome/content/zotero/xpcom/sync.js +++ b/chrome/content/zotero/xpcom/sync.js @@ -505,12 +505,12 @@ Zotero.Sync.Runner = new function () { var _autoSyncTimer; var _queue; - var _running; var _background; var _lastSyncStatus; var _currentSyncStatusLabel; var _currentLastSyncLabel; + var _errorsByLibrary = {}; var _warning = null; @@ -526,16 +526,9 @@ Zotero.Sync.Runner = new function () { this.clearSyncTimeout(); // DEBUG: necessary? var msg = "Zotero cannot sync while " + Zotero.appName + " is in offline mode."; var e = new Zotero.Error(msg, 0, { dialogButtonText: null }) - this.setSyncIcon('error', e); - return false; - } - - if (_running) { - // TODO: show status in all windows - var msg = "A sync process is already running. To view progress, check " - + "the window in which the sync began or restart " + Zotero.appName + "."; - var e = new Zotero.Error(msg, 0, { dialogButtonText: null, frontWindowOnly: true }) - this.setSyncIcon('error', e); + Components.utils.reportError(e); + Zotero.debug(e, 1); + this.setSyncIcon(e); return false; } @@ -543,7 +536,6 @@ Zotero.Sync.Runner = new function () { Zotero.purgeDataObjects(true); _background = !!background; - _running = true; this.setSyncIcon('animate'); var finalCallbacks = { @@ -554,61 +546,30 @@ Zotero.Sync.Runner = new function () { }; var storageSync = function () { - var syncNeeded = false; - Zotero.Sync.Runner.setSyncStatus(Zotero.getString('sync.status.syncingFiles')); - var zfsSync = function (skipSyncNeeded) { - Zotero.Sync.Storage.ZFS.sync({ - // ZFS success - onSuccess: function () { - setTimeout(function () { - Zotero.Sync.Server.sync(finalCallbacks); - }, 0); - }, - - // ZFS skip - onSkip: function () { - setTimeout(function () { - if (skipSyncNeeded) { - Zotero.Sync.Server.sync(finalCallbacks); - } - else { - Zotero.Sync.Runner.stop(); - } - }, 0); - }, - - // ZFS cancel - onStop: function () { - setTimeout(function () { - Zotero.Sync.Runner.stop(); - }, 0); - }, - - // ZFS failure - onError: Zotero.Sync.Runner.error, - - onWarning: Zotero.Sync.Runner.warning - }) - }; - - Zotero.Sync.Storage.WebDAV.sync({ - // WebDAV success - onSuccess: function () { - zfsSync(true); - }, + Zotero.Sync.Storage.sync() + .then(function (results) { + Zotero.debug("File sync is finished"); - // WebDAV skip - onSkip: function () { - zfsSync(); - }, + if (results.errors.length) { + Zotero.Sync.Runner.setErrors(results.errors); + + return; + } - // WebDAV cancel - onStop: Zotero.Sync.Runner.stop, - - // WebDAV failure - onError: Zotero.Sync.Runner.error + if (results.changesMade) { + Zotero.debug("Changes made during file sync " + + "-- performing additional data sync"); + Zotero.Sync.Server.sync(finalCallbacks); + } + else { + Zotero.Sync.Runner.stop(); + } + }) + .fail(function (e) { + Zotero.debug("File sync failed", 1); + Zotero.Sync.Runner.error(e); }); }; @@ -620,23 +581,26 @@ Zotero.Sync.Runner = new function () { onSkip: storageSync, // Sync 1 stop - onStop: Zotero.Sync.Runner.stop, + onStop: function () { + Zotero.Sync.Runner.stop(); + }, // Sync 1 error - onError: Zotero.Sync.Runner.error + onError: function (e) { + Zotero.Sync.Runner.error(e); + } }); } this.stop = function () { if (_warning) { - Zotero.Sync.Runner.setSyncIcon('warning', _warning); + Zotero.Sync.Runner.setSyncIcon(_warning); _warning = null; } else { Zotero.Sync.Runner.setSyncIcon(); } - _running = false; } @@ -644,14 +608,17 @@ Zotero.Sync.Runner = new function () { * Log a warning, but don't throw an error */ this.warning = function (e) { + Zotero.debug(e, 2); Components.utils.reportError(e); + e.status = 'warning'; _warning = e; } this.error = function (e) { - Zotero.Sync.Runner.setSyncIcon('error', e); - _running = false; + Components.utils.reportError(e); + Zotero.debug(e, 1); + Zotero.Sync.Runner.setSyncIcon(e); throw (e); } @@ -740,60 +707,85 @@ Zotero.Sync.Runner = new function () { } - this.setSyncIcon = function (status, e) { - var message; - var buttonText; - var buttonCallback; - var frontWindowOnly = false; + /** + * Trigger updating of the main sync icon, the sync error icon, and + * library-specific sync error icons across all windows + */ + this.setErrors = function (errors) { + Zotero.debug(errors); + errors = [this.parseSyncError(e) for each(e in errors)]; + Zotero.debug(errors); + _errorsByLibrary = {}; - status = status ? status : ''; + var primaryError = this.getPrimaryError(errors); + Zotero.debug(primaryError); + this.setSyncIcon(primaryError); - switch (status) { - case '': - case 'animate': - case 'warning': - case 'error': - break; + // Store other errors by libraryID to be shown in the source list + for each(var e in errors) { + // Skip non-library-specific errors + if (typeof e.libraryID == 'undefined') { + continue; + } - default: - throw ("Invalid sync icon status '" + status - + "' in Zotero.Sync.Runner.setSyncIcon()"); + if (!_errorsByLibrary[e.libraryID]) { + _errorsByLibrary[e.libraryID] = []; + } + _errorsByLibrary[e.libraryID].push(e); } - if (e) { - if (e.data) { - if (e.data.dialogText) { - message = e.data.dialogText; - } - if (typeof e.data.dialogButtonText != 'undefined') { - buttonText = e.data.dialogButtonText; - buttonCallback = e.data.dialogButtonCallback; - } - if (e.data.frontWindowOnly) { - frontWindowOnly = e.data.frontWindowOnly; - } + // Refresh source list + Zotero.Notifier.trigger('redraw', 'collection', []); + } + + + this.getErrors = function (libraryID) { + if (!_errorsByLibrary[libraryID]) { + return false; + } + return _errorsByLibrary[libraryID]; + } + + + this.getPrimaryError = function (errors) { + errors = [this.parseSyncError(e) for each(e in errors)]; + + // Set highest priority error as the primary (sync error icon) + var statusPriorities = { + info: 1, + warning: 2, + error: 3, + upgrade: 4, + + // Skip these + animate: -1 + }; + var primaryError = false; + for each(var error in errors) { + if (!error.status || statusPriorities[error.status] == -1) { + continue; } - if (!message) { - if (e.message) { - message = e.message; - } - else { - message = e; - } + if (!primaryError || statusPriorities[error.status] + > statusPriorities[primaryError.status]) { + primaryError = error; } } + return primaryError; + } + + + /** + * Set the main sync error icon across all windows + */ + this.setSyncIcon = function (e) { + e = this.parseSyncError(e); - var upgradeRequired = false; if (Zotero.Sync.Server.upgradeRequired) { - upgradeRequired = true; + e.status = 'upgrade'; Zotero.Sync.Server.upgradeRequired = false; } - if (status == 'error') { - var errorsLogged = Zotero.getErrors().length > 0; - } - - if (frontWindowOnly) { + if (e.frontWindowOnly) { // Fake an nsISimpleEnumerator with just the topmost window var enumerator = { _returned: false, @@ -820,100 +812,17 @@ Zotero.Sync.Runner = new function () { while (enumerator.hasMoreElements()) { var win = enumerator.getNext(); - if(!win.ZoteroPane) continue; - var warning = win.ZoteroPane.document.getElementById('zotero-tb-sync-warning'); - var icon = win.ZoteroPane.document.getElementById('zotero-tb-sync'); + if (!win.ZoteroPane) continue; + var doc = win.ZoteroPane.document; - if (status == 'warning' || status == 'error') { - icon.setAttribute('status', ''); - warning.hidden = false; - if (upgradeRequired) { - warning.setAttribute('mode', 'upgrade'); - buttonText = null; - } - else { - warning.setAttribute('mode', status); - } - warning.tooltipText = message; - warning.onclick = function () { - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - var win = wm.getMostRecentWindow("navigator:browser"); - - var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService); - // Warning - if (status == 'warning') { - var title = Zotero.getString('general.warning'); - - // If secondary button not specified, just use an alert - if (!buttonText) { - ps.alert(null, title, message); - return; - } - - var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK - + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING; - var index = ps.confirmEx( - null, - title, - message, - buttonFlags, - "", - buttonText, - "", null, {} - ); - - if (index == 1) { - setTimeout(function () { buttonCallback(); }, 1); - } - } - - // Error - else if (status == 'error') { - // Probably not necessary, but let's be sure - if (!errorsLogged) { - Components.utils.reportError(message); - } - - if (typeof buttonText == 'undefined') { - buttonText = Zotero.getString('errorReport.reportError'); - buttonCallback = function () { - win.ZoteroPane.reportErrors(); - } - } - // If secondary button is explicitly null, just use an alert - else if (buttonText === null) { - ps.alert(null, title, message); - return; - } - - var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK - + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING; - var index = ps.confirmEx( - null, - Zotero.getString('general.error'), - message, - buttonFlags, - "", - buttonText, - "", null, {} - ); - - if (index == 1) { - setTimeout(function () { buttonCallback(); }, 1); - } - } - } - } - else { - icon.setAttribute('status', status); - warning.hidden = true; - warning.onclick = null; - } + var button = doc.getElementById('zotero-tb-sync-error'); + this.setErrorIcon(button, [e]); + var syncIcon = doc.getElementById('zotero-tb-sync'); + // Update sync icon state + syncIcon.setAttribute('status', e.status ? e.status : ""); // Disable button while spinning - icon.disabled = status == 'animate'; + syncIcon.disabled = e.status == 'animate'; } // Clear status @@ -921,6 +830,9 @@ Zotero.Sync.Runner = new function () { } + /** + * Set the sync icon tooltip message + */ this.setSyncStatus = function (msg) { _lastSyncStatus = msg; @@ -931,6 +843,132 @@ Zotero.Sync.Runner = new function () { } + this.parseSyncError = function (e) { + if (!e) { + return { parsed: true }; + } + + var parsed = { + parsed: true + }; + + // In addition to actual errors, string states (e.g., 'animate') + // can be passed + if (typeof e == 'string') { + parsed.status = e; + return parsed; + } + + // Already parsed + if (e.parsed) { + return e; + } + + if (typeof e.libraryID != 'undefined') { + parsed.libraryID = e.libraryID; + } + parsed.status = e.status ? e.status : 'error'; + + if (e.data) { + if (e.data.dialogText) { + parsed.message = e.data.dialogText; + } + if (typeof e.data.dialogButtonText != 'undefined') { + parsed.buttonText = e.data.dialogButtonText; + parsed.buttonCallback = e.data.dialogButtonCallback; + } + } + if (!parsed.message) { + parsed.message = e.message ? e.message : e; + } + + parsed.frontWindowOnly = !!(e && e.data && e.data.frontWindowOnly); + + return parsed; + } + + + /** + * Set the state of the sync error icon and add an onclick to populate + * the error panel + */ + this.setErrorIcon = function (icon, errors) { + if (!errors || !errors.length) { + icon.hidden = true; + icon.onclick = null; + return; + } + + // TEMP: for now, use the first error + var e = this.getPrimaryError(errors); + + if (!e.status) { + icon.hidden = true; + icon.onclick = null; + return; + } + + icon.hidden = false; + icon.setAttribute('mode', e.status); + icon.onclick = function () { + var doc = this.ownerDocument; + + var panel = Zotero.Sync.Runner.updateErrorPanel(doc, errors); + + panel.openPopup(this, "after_end", 4, 0, false, false); + } + } + + + this.updateErrorPanel = function (doc, errors) { + var panel = doc.getElementById('zotero-sync-error-panel'); + var panelContent = doc.getElementById('zotero-sync-error-panel-content'); + var panelButtons = doc.getElementById('zotero-sync-error-panel-buttons'); + + // Clear existing panel content + while (panelContent.hasChildNodes()) { + panelContent.removeChild(panelContent.firstChild); + } + while (panelButtons.hasChildNodes()) { + panelButtons.removeChild(panelButtons.firstChild); + } + + // TEMP: for now, we only show one error + var e = errors.concat().shift(); + e = this.parseSyncError(e); + + var desc = doc.createElement('description'); + desc.textContent = e.message; + panelContent.appendChild(desc); + + // If not an error and there's no explicit button text, don't show + // button to report errors + if (e.status != 'error' && typeof e.buttonText == 'undefined') { + e.buttonText = null; + } + + if (e.buttonText !== null) { + if (typeof e.buttonText == 'undefined') { + var buttonText = Zotero.getString('errorReport.reportError'); + var buttonCallback = function () { + doc.defaultView.ZoteroPane.reportErrors(); + }; + } + else { + var buttonText = e.buttonText; + var buttonCallback = e.buttonCallback; + } + + var button = doc.createElement('button'); + button.setAttribute('label', buttonText); + button.onclick = buttonCallback; + panelButtons.appendChild(button); + } + + return panel; + } + + /** * Register label in sync icon tooltip to receive updates * @@ -1440,7 +1478,7 @@ Zotero.Sync.Server = new function () { Zotero.suppressUIUpdates = true; _updatesInProgress = true; - var errorHandler = function (e) { + var errorHandler = function (e, rethrow) { Zotero.DB.rollbackTransaction(); Zotero.UnresponsiveScriptIndicator.enable(); @@ -1451,6 +1489,9 @@ Zotero.Sync.Server = new function () { Zotero.suppressUIUpdates = false; _updatesInProgress = false; + if (rethrow) { + throw (e); + } _error(e); } @@ -1662,7 +1703,7 @@ Zotero.Sync.Server = new function () { Zotero.pumpGenerator(gen, false, errorHandler); } catch (e) { - errorHandler(e); + errorHandler(e, true); } } catch (e) { @@ -2987,17 +3028,16 @@ Zotero.Sync.Server.Data = new function() { obj.attachmentSyncState = Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD; } - // Set existing attachments mtime update check + // Set existing attachments for mtime update check else { var mtime = objectNode.getAttribute('storageModTime'); if (mtime) { - var lk = Zotero.Items.getLibraryKeyHash(obj) // Convert previously used Unix timestamps to ms-based timestamps if (mtime < 10000000000) { Zotero.debug("Converting Unix timestamp '" + mtime + "' to milliseconds"); mtime = mtime * 1000; } - itemStorageModTimes[lk] = parseInt(mtime); + itemStorageModTimes[obj.id] = parseInt(mtime); } } } @@ -3313,18 +3353,8 @@ Zotero.Sync.Server.Data = new function() { // Check mod times and hashes of updated items against stored values to see // if they've been updated elsewhere and mark for download if so - if (type == 'item') { - var ids = []; - var modTimes = {}; - for (var libraryKeyHash in itemStorageModTimes) { - var lk = Zotero.Items.parseLibraryKeyHash(libraryKeyHash); - var item = Zotero.Items.getByLibraryAndKey(lk.libraryID, lk.key); - ids.push(item.id); - modTimes[item.id] = itemStorageModTimes[libraryKeyHash]; - } - if (ids.length > 0) { - Zotero.Sync.Storage.checkForUpdatedFiles(ids, modTimes); - } + if (type == 'item' && Object.keys(itemStorageModTimes).length) { + Zotero.Sync.Storage.checkForUpdatedFiles(itemStorageModTimes); } } diff --git a/chrome/content/zotero/xpcom/uri.js b/chrome/content/zotero/xpcom/uri.js index 6eb53393f..38511214a 100644 --- a/chrome/content/zotero/xpcom/uri.js +++ b/chrome/content/zotero/xpcom/uri.js @@ -83,12 +83,9 @@ Zotero.URI = new function () { * Get path portion of library URI (e.g., users/6 or groups/1) */ this.getLibraryPath = function (libraryID) { - if (libraryID) { - var libraryType = Zotero.Libraries.getType(libraryID); - } - else { - libraryType = 'user'; - } + libraryID = libraryID ? parseInt(libraryID) : 0; + var libraryType = Zotero.Libraries.getType(libraryID); + switch (libraryType) { case 'user': var id = Zotero.userID; diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 77f21169d..bf206d409 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -153,6 +153,7 @@ var ZoteroPane = new function() var collectionsTree = document.getElementById('zotero-collections-tree'); collectionsTree.view = ZoteroPane_Local.collectionsView; collectionsTree.controllers.appendController(new Zotero.CollectionTreeCommandController(collectionsTree)); + collectionsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true); collectionsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true); var itemsTree = document.getElementById('zotero-items-tree'); @@ -2509,11 +2510,32 @@ var ZoteroPane = new function() var t = event.originalTarget; var tree = t.parentNode; - var itemGroup = ZoteroPane_Local.getItemGroup(); + var row = {}, col = {}, obj = {}; + tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj); + if (row.value == -1) { + return; + } + + var itemGroup = ZoteroPane_Local.collectionsView._getItemAtRow(row.value); + + // Prevent the tree's select event from being called for a click + // on a library sync error icon + if (tree.id == 'zotero-collections-tree') { + if (itemGroup.isLibrary(true)) { + if (col.value.id == 'zotero-collections-sync-status-column') { + var libraryID = itemGroup.isLibrary() ? 0 : itemGroup.ref.libraryID; + var errors = Zotero.Sync.Runner.getErrors(libraryID); + if (errors) { + event.stopPropagation(); + return; + } + } + } + } // Automatically select all equivalent items when clicking on an item // in duplicates view - if (itemGroup.isDuplicates() && tree.id == 'zotero-items-tree') { + else if (tree.id == 'zotero-items-tree' && itemGroup.isDuplicates()) { // Trigger only on primary-button single clicks with modifiers // (so that items can still be selected and deselected manually) if (!event || event.detail != 1 || event.button != 0 || event.metaKey || event.shiftKey) { @@ -2558,22 +2580,52 @@ var ZoteroPane = new function() var tree = t.parentNode; + var row = {}, col = {}, obj = {}; + tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj); + // We care only about primary-button double and triple clicks if (!event || (event.detail != 2 && event.detail != 3) || event.button != 0) { + if (row.value == -1) { + return; + } + var itemGroup = ZoteroPane_Local.collectionsView._getItemAtRow(row.value); + + // Show the error panel when clicking a library-specific + // sync error icon + if (itemGroup.isLibrary(true)) { + if (col.value.id == 'zotero-collections-sync-status-column') { + var libraryID = itemGroup.isLibrary() ? 0 : itemGroup.ref.libraryID; + var errors = Zotero.Sync.Runner.getErrors(libraryID); + if (!errors) { + return; + } + + var panel = Zotero.Sync.Runner.updateErrorPanel(window.document, errors); + + var anchor = document.getElementById('zotero-collections-tree-shim'); + + var x = {}, y = {}, width = {}, height = {}; + tree.treeBoxObject.getCoordsForCellItem(row.value, col.value, 'image', x, y, width, height); + + x = x.value + Math.round(width.value / 2); + y = y.value + height.value + 3; + + panel.openPopup(anchor, "after_start", x, y, false, false); + } + + return; + } + // The Mozilla tree binding fires select() in mousedown(), // but if when it gets to click() the selection differs from // what it expects (say, because multiple items had been - // selected during mousedown()), it fires select() again. - // We prevent that here. - var itemGroup = ZoteroPane_Local.getItemGroup(); - if (itemGroup.isDuplicates() && tree.id == 'zotero-items-tree') { + // selected during mousedown(), as is the case in duplicates mode), + // it fires select() again. We prevent that here. + else if (itemGroup.isDuplicates() && tree.id == 'zotero-items-tree') { if (event.metaKey || event.shiftKey) { return; } - // Allow twisty click to work in duplicates mode - var row = {}, col = {}, obj = {}; - tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj); if (obj.value == 'twisty') { return; } @@ -2597,9 +2649,6 @@ var ZoteroPane = new function() } } - var row = {}, col = {}, obj = {}; - tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj); - // obj.value == 'cell'/'text'/'image' if (!obj.value) { return; @@ -3424,6 +3473,8 @@ var ZoteroPane = new function() function viewAttachment(itemIDs, event, noLocateOnMissing, forceExternalViewer) { + Components.utils.import("resource://zotero/q.js"); + // If view isn't editable, don't show Locate button, since the updated // path couldn't be sent back up if (!this.collectionsView.editable) { @@ -3478,38 +3529,39 @@ var ZoteroPane = new function() } } else { - if (item.isImportedAttachment() && Zotero.Sync.Storage.downloadAsNeeded(item.libraryID)) { - let downloadedItem = item; - var started = Zotero.Sync.Storage.downloadFile(item, { - onStart: function (request) { - if (!(request instanceof Zotero.Sync.Storage.Request)) { - throw new Error("Invalid request object"); - } - }, - - onProgress: function (progress, progressMax) { - - }, - - onStop: function () { - if (!downloadedItem.getFile()) { - ZoteroPane_Local.showAttachmentNotFoundDialog(itemID, noLocateOnMissing); - return; - } - - // check if unchanged? - // maybe not necessary, since we'll get an error if there's an error - - ZoteroPane_Local.viewAttachment(downloadedItem.id, event, false, forceExternalViewer); - }, - }); - - if (started) { - continue; - } + if (!item.isImportedAttachment() || !Zotero.Sync.Storage.downloadAsNeeded(item.libraryID)) { + this.showAttachmentNotFoundDialog(itemID, noLocateOnMissing); + return; } - this.showAttachmentNotFoundDialog(itemID, noLocateOnMissing); + let downloadedItem = item; + Q.fcall(function () { + return Zotero.Sync.Storage.downloadFile( + downloadedItem, + { + onProgress: function (progress, progressMax) {} + }); + }) + .then(function () { + if (!downloadedItem.getFile()) { + ZoteroPane_Local.showAttachmentNotFoundDialog(downloadedItem.id, noLocateOnMissing); + return; + } + + // check if unchanged? + // maybe not necessary, since we'll get an error if there's an error + + + Zotero.Notifier.trigger('redraw', 'item', []); + + ZoteroPane_Local.viewAttachment(downloadedItem.id, event, false, forceExternalViewer); + }) + .fail(function (e) { + // TODO: show error somewhere else + Zotero.debug(e, 1); + ZoteroPane_Local.syncAlert(e); + }) + .end(); } } } @@ -3744,6 +3796,83 @@ var ZoteroPane = new function() } + this.syncAlert = function (e) { + e = Zotero.Sync.Runner.parseSyncError(e); + + var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK + + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING; + + // Warning + if (e.status == 'warning') { + var title = Zotero.getString('general.warning'); + + // If secondary button not specified, just use an alert + if (e.buttonText) { + var buttonText = e.buttonText; + } + else { + ps.alert(null, title, e.message); + return; + } + + var index = ps.confirmEx( + null, + title, + e.message, + buttonFlags, + "", + buttonText, + "", null, {} + ); + + if (index == 1) { + setTimeout(function () { buttonCallback(); }, 1); + } + } + // Error + else if (e.status == 'error') { + var title = Zotero.getString('general.error'); + + // If secondary button is explicitly null, just use an alert + if (buttonText === null) { + ps.alert(null, title, e.message); + return; + } + + if (typeof buttonText == 'undefined') { + var buttonText = Zotero.getString('errorReport.reportError'); + var buttonCallback = function () { + ZoteroPane.reportErrors(); + }; + } + else { + var buttonText = e.buttonText; + var buttonCallback = e.buttonCallback; + } + + var index = ps.confirmEx( + null, + title, + e.message, + buttonFlags, + "", + buttonText, + "", null, {} + ); + + if (index == 1) { + setTimeout(function () { buttonCallback(); }, 1); + } + } + // Upgrade + else if (e.status == 'upgrade') { + ps.alert(null, "", e.message); + } + }; + + this.createParentItemsFromSelected = function () { if (!this.canEdit()) { this.displayCannotEditLibraryMessage(); diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul index 84a7ba4dd..aebfe7f96 100644 --- a/chrome/content/zotero/zoteroPane.xul +++ b/chrome/content/zotero/zoteroPane.xul @@ -192,32 +192,22 @@ value="0" tooltip="zotero-tb-sync-progress-tooltip"> - - - - - - - - - - - - - - + + + -