From f8e995409f75ccd96318cf92aad442e292bc1ada Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Wed, 25 Nov 2009 06:33:10 +0000 Subject: [PATCH] - Support server-based sync request queuing - Fix "Module not initialized" error - Display sync status in sync icon tooltip --- chrome/content/zotero/overlay.js | 17 - chrome/content/zotero/overlay.xul | 6 +- chrome/content/zotero/xpcom/storage.js | 3 +- chrome/content/zotero/xpcom/sync.js | 421 +++++++++++-------------- chrome/skin/default/zotero/overlay.css | 2 +- 5 files changed, 196 insertions(+), 253 deletions(-) diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js index 240acf406e..49b2fbe7db 100644 --- a/chrome/content/zotero/overlay.js +++ b/chrome/content/zotero/overlay.js @@ -3113,23 +3113,6 @@ var ZoteroPane = new function() } - this.setLastSyncStatus = function (tooltip) { - var label = tooltip.firstChild.nextSibling; - - var lastSyncTime = Zotero.Sync.Server.lastLocalSyncTime; - // TODO: localize - msg = 'Last sync: '; - if (lastSyncTime) { - var time = new Date(lastSyncTime * 1000); - msg += Zotero.Date.toRelativeDate(time); - } - else { - msg += 'Not yet synced'; - } - label.value = msg; - } - - function reportErrors() { var errors = Zotero.getErrors(true); var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] diff --git a/chrome/content/zotero/overlay.xul b/chrome/content/zotero/overlay.xul index 5c9cc539e9..1e01c1c348 100644 --- a/chrome/content/zotero/overlay.xul +++ b/chrome/content/zotero/overlay.xul @@ -390,11 +390,13 @@ diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js index 689a914b9d..cd23c3da29 100644 --- a/chrome/content/zotero/xpcom/storage.js +++ b/chrome/content/zotero/xpcom/storage.js @@ -701,7 +701,8 @@ Zotero.Sync.Storage = new function () { this.purgeDeletedStorageFiles = function (module, callback) { _session = new Zotero.Sync.Storage.Session(module, { onError: _error }); if (!_session.initFromPrefs()) { - _error("Module not initialized"); + Zotero.debug("Module '" + module + "' not initialized in Zotero.Sync.Storage.purgeDeletedStorageFiles()"); + return; } _session.purgeDeletedStorageFiles(callback); } diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js index 1292b6209a..40701310b3 100644 --- a/chrome/content/zotero/xpcom/sync.js +++ b/chrome/content/zotero/xpcom/sync.js @@ -455,11 +455,18 @@ Zotero.Sync.Runner = new function () { return _background; }); + this.__defineGetter__("lastSyncStatus", function () { + return _lastSyncStatus; + }); + var _autoSyncTimer; var _queue; var _running; var _background; + var _lastSyncStatus; + var _currentSyncStatusLabel; + var _warning = null; this.init = function () { @@ -490,6 +497,9 @@ Zotero.Sync.Runner = new function () { var storageSync = function () { var syncNeeded = false; + // TODO: localize + Zotero.Sync.Runner.setSyncStatus("Syncing files"); + Zotero.Sync.Storage.sync( 'webdav', @@ -823,6 +833,59 @@ Zotero.Sync.Runner = new function () { // Disable button while spinning icon.disabled = status == 'animate'; + + // Clear tooltip + _tooltip = null; + + // Clear status + Zotero.Sync.Runner.setSyncStatus(); + } + + + this.setSyncStatus = function (msg) { + _lastSyncStatus = msg; + + // If a label is registered, update it + if (_currentSyncStatusLabel) { + _updateSyncStatusLabel(); + } + } + + + /** + * Register label in sync icon tooltip to receive updates + * + * If no label passed, unregister current label + * + * @param {Tooltip} [label] + */ + this.registerSyncStatusLabel = function (label) { + _currentSyncStatusLabel = label; + if (label) { + _updateSyncStatusLabel(); + } + } + + + function _updateSyncStatusLabel() { + if (_lastSyncStatus) { + var msg = _lastSyncStatus; + } + // If no message, use last sync time + else { + var lastSyncTime = Zotero.Sync.Server.lastLocalSyncTime; + // TODO: localize + var msg = 'Last sync: '; + if (lastSyncTime) { + var time = new Date(lastSyncTime * 1000); + msg += Zotero.Date.toRelativeDate(time); + } + else { + msg += 'Not yet synced'; + } + } + + _currentSyncStatusLabel.value = msg; } } @@ -924,10 +987,7 @@ Zotero.Sync.Runner.IdleListener = { Zotero.Sync.Server = new function () { this.login = login; this.sync = sync; - this.lock = lock; - this.unlock = unlock; this.clear = clear; - this.resetServer = resetServer; this.resetClient = resetClient; this.logout = logout; @@ -1036,8 +1096,9 @@ Zotero.Sync.Server = new function () { Zotero.DB.query("REPLACE INTO version VALUES ('lastlocalsync', ?)", { int: val }); }); + this.nextLocalSyncDate = false; - this.apiVersion = 5; + this.apiVersion = 6; default xml namespace = ''; @@ -1050,8 +1111,8 @@ Zotero.Sync.Server = new function () { var _cachedCredentials = {}; var _syncInProgress; var _sessionID; - var _sessionLock; var _throttleTimeout; + var _checkTimer; var _canAutoResetClient = true; var _callbacks = { @@ -1092,6 +1153,9 @@ Zotero.Sync.Server = new function () { + "&username=" + username + "&password=" + password; + // TODO: localize + Zotero.Sync.Runner.setSyncStatus("Logging in to sync server"); + Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { _checkResponse(xmlhttp); @@ -1129,7 +1193,7 @@ Zotero.Sync.Server = new function () { } - function sync(callbacks) { + function sync(callbacks, restart, upload) { for (var func in callbacks) { _callbacks[func] = callbacks[func]; } @@ -1146,19 +1210,15 @@ Zotero.Sync.Server = new function () { return; } - /* - if (!_sessionLock) { - Zotero.Sync.Server.lock(Zotero.Sync.Server.sync, callback); - return; + if (!restart) { + if (_syncInProgress) { + // TODO: localize + _error("A sync operation is already in progress. Wait for the previous sync to complete or restart Firefox."); + } + + Zotero.debug("Beginning server sync"); + _syncInProgress = true; } - */ - - if (_syncInProgress) { - _error("Sync operation already in progress"); - } - - Zotero.debug("Beginning server sync"); - _syncInProgress = true; // Get updated data var url = _serverURL + 'updated'; @@ -1169,13 +1229,21 @@ Zotero.Sync.Server = new function () { } var body = _apiVersionComponent + '&' + Zotero.Sync.Server.sessionIDComponent - + '&lastsync=' + lastsync - + '&lock=1'; + + '&lastsync=' + lastsync; + // Tell server to check for read locks as well as write locks, + // since we'll be uploading + if (upload) { + body += '&upload=1'; + } + + // TODO: localize + Zotero.Sync.Runner.setSyncStatus("Getting updated data from sync server"); Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { Zotero.debug(xmlhttp.responseText); _checkResponse(xmlhttp); + if (_invalidSession(xmlhttp)) { Zotero.debug("Invalid session ID -- logging in"); _sessionID = false; @@ -1188,22 +1256,22 @@ Zotero.Sync.Server = new function () { var response = xmlhttp.responseXML.childNodes[0]; - if (response.firstChild.tagName == 'error') { - var resetCallback = function () { - Zotero.Sync.Server.sync(_callbacks); - }; - if (_checkServerSessionLock(response.firstChild, resetCallback)) { - return; - } - _error(response.firstChild.firstChild.nodeValue); + // If server session is locked, keep checking back + if (_checkServerLock(response, function () { Zotero.Sync.Server.sync(_callbacks, true, upload); })) { + return; } - _sessionLock = true; + // Error that's not handled by _checkResponse() + if (response.firstChild.localName == 'error') { + _error(response.firstChild.firstChild.nodeValue); + } // Strip XML declaration and convert to E4X var xml = new XML(xmlhttp.responseText.replace(/<\?xml.*\?>/, '')); try { + var updateKey = xml.@updateKey.toString(); + // If no earliest date is provided by the server, the server // account is empty var earliestRemoteDate = parseInt(xml.@earliest) ? @@ -1223,10 +1291,8 @@ Zotero.Sync.Server = new function () { } else if (c == -1) { Zotero.debug("Sync cancelled"); - Zotero.Sync.Server.unlock(function () { - _callbacks.onStop(); - }); _syncInProgress = false; + _callbacks.onStop(); return; } @@ -1257,6 +1323,9 @@ Zotero.Sync.Server = new function () { var nextLocalSyncTime = Zotero.Date.toUnixTimestamp(nextLocalSyncDate); Zotero.Sync.Server.nextLocalSyncDate = nextLocalSyncDate; + // TODO: localize + Zotero.Sync.Runner.setSyncStatus("Processing updated data"); + // Reconcile and save updated data from server and // prepare local data to upload var xmlstr = Zotero.Sync.Server.Data.processUpdatedXML( @@ -1269,12 +1338,10 @@ Zotero.Sync.Server = new function () { if (xmlstr === false) { Zotero.debug("Sync cancelled"); Zotero.DB.rollbackTransaction(); - Zotero.Sync.Server.unlock(function () { - _callbacks.onStop(); - }); Zotero.reloadDataObjects(); Zotero.Sync.EventListener.resetIgnored(); _syncInProgress = false; + _callbacks.onStop(); return; } @@ -1289,18 +1356,20 @@ Zotero.Sync.Server = new function () { Zotero.Sync.Server.lastLocalSyncTime = nextLocalSyncTime; Zotero.Sync.Server.nextLocalSyncDate = false; Zotero.DB.commitTransaction(); - Zotero.Sync.Server.unlock(function () { - _syncInProgress = false; - _callbacks.onSuccess(); - }); + _syncInProgress = false; + _callbacks.onSuccess(); return; } Zotero.DB.commitTransaction(); + // TODO: localize + Zotero.Sync.Runner.setSyncStatus("Uploading data to sync server"); + var url = _serverURL + 'upload'; var body = _apiVersionComponent + '&' + Zotero.Sync.Server.sessionIDComponent + + '&updateKey=' + updateKey + '&data=' + encodeURIComponent(xmlstr); //var file = Zotero.getZoteroDirectory(); @@ -1308,19 +1377,57 @@ Zotero.Sync.Server = new function () { //Zotero.File.putContents(file, body); var uploadCallback = function (xmlhttp) { + if (xmlhttp.status == 409) { + Zotero.debug("Upload key is no longer valid -- restarting sync"); + setTimeout(function () { + Zotero.Sync.Server.sync(_callbacks, true, true); + }, 1); + return; + } + _checkResponse(xmlhttp); - //var ZU = new Zotero.Utilities; - //Zotero.debug(ZU.unescapeHTML(xmlhttp.responseText)); Zotero.debug(xmlhttp.responseText); - var response = xmlhttp.responseXML.childNodes[0]; + if (_checkServerLock(response, function (mode) { + switch (mode) { + // If the upload was queued, keep checking back + case 'queued': + // TODO: localize + Zotero.Sync.Runner.setSyncStatus("Upload accepted \u2014 waiting for sync server"); + + var url = _serverURL + 'uploadstatus'; + var body = _apiVersionComponent + + '&' + Zotero.Sync.Server.sessionIDComponent; + Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { + uploadCallback(xmlhttp); + }); + break; + + // If affected libraries were locked, restart sync, + // since the upload key would be out of date anyway + case 'locked': + setTimeout(function () { + Zotero.Sync.Server.sync(_callbacks, true, true); + }, 1); + break; + + default: + throw ("Unexpected server lock mode '" + mode + "' in Zotero.Sync.Server.upload()"); + } + })) { return; } + if (response.firstChild.tagName == 'error') { // handle error _error(response.firstChild.firstChild.nodeValue); } + if (response.firstChild.localName != 'uploaded') { + _error("Unexpected upload response '" + response.firstChild.localName + + "' in Zotero.Sync.Server.sync()"); + } + Zotero.DB.beginTransaction(); Zotero.Sync.purgeDeletedObjects(nextLocalSyncTime); Zotero.Sync.Server.lastLocalSyncTime = nextLocalSyncTime; @@ -1330,10 +1437,8 @@ Zotero.Sync.Server = new function () { //throw('break2'); Zotero.DB.commitTransaction(); - Zotero.Sync.Server.unlock(function () { - _syncInProgress = false; - _callbacks.onSuccess(); - }); + _syncInProgress = false; + _callbacks.onSuccess(); } var compress = Zotero.Prefs.get('sync.server.compressData'); @@ -1415,96 +1520,6 @@ Zotero.Sync.Server = new function () { } - function lock(callback) { - Zotero.debug("Getting session lock"); - - if (!_sessionID) { - _error('No session available in Zotero.Sync.Server.lock()', 2); - } - - if (_sessionLock) { - _error('Session already locked in Zotero.Sync.Server.lock()', 2); - } - - var url = _serverURL + "lock"; - var body = _apiVersionComponent - + '&' + Zotero.Sync.Server.sessionIDComponent; - - Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { - if (_invalidSession(xmlhttp)) { - Zotero.debug("Invalid session ID -- logging in"); - _sessionID = false; - Zotero.Sync.Server.login(callback); - return; - } - - _checkResponse(xmlhttp); - - Zotero.debug(xmlhttp.responseText); - - var response = xmlhttp.responseXML.childNodes[0]; - - if (response.firstChild.tagName == 'error') { - if (_checkServerSessionLock(response.firstChild)) { - Zotero.Sync.Server.lock(callback ? function () { callback(); } : null); - return; - } - - _error(response.firstChild.firstChild.nodeValue); - } - - if (response.firstChild.tagName != 'locked') { - _error('Invalid response from server', xmlhttp.responseText); - } - - _sessionLock = true; - - if (callback) { - callback(); - } - }); - } - - - function unlock(callback) { - Zotero.debug("Releasing session lock"); - - if (!_sessionID) { - _error('No session available in Zotero.Sync.Server.unlock()'); - } - - var syncInProgress = _syncInProgress; - - var url = _serverURL + "unlock"; - var body = _apiVersionComponent - + '&' + Zotero.Sync.Server.sessionIDComponent; - - Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { - _checkResponse(xmlhttp); - - Zotero.debug(xmlhttp.responseText); - - var response = xmlhttp.responseXML.childNodes[0]; - - if (response.firstChild.tagName == 'error') { - _sessionLock = null; - _error(response.firstChild.firstChild.nodeValue); - } - - if (response.firstChild.tagName != 'unlocked') { - _sessionLock = null; - _error('Invalid response from server', xmlhttp.responseText); - } - - _sessionLock = null; - - if (callback) { - callback(); - } - }); - } - - function clear(callback) { if (!_sessionID) { Zotero.debug("Session ID not available -- logging in"); @@ -1549,55 +1564,6 @@ Zotero.Sync.Server = new function () { } - /** - * Clear session lock on server - */ - function resetServer(callback) { - if (!_sessionID) { - Zotero.debug("Session ID not available -- logging in"); - Zotero.Sync.Server.login(function () { - Zotero.Sync.Server.resetServer(callback); - }); - return; - } - - var url = _serverURL + "reset"; - var body = _apiVersionComponent - + '&' + Zotero.Sync.Server.sessionIDComponent; - - Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { - if (_invalidSession(xmlhttp)) { - Zotero.debug("Invalid session ID -- logging in"); - _sessionID = false; - Zotero.Sync.Server.login(function () { - Zotero.Sync.Server.reset(); - }); - return; - } - - _checkResponse(xmlhttp); - - Zotero.debug(xmlhttp.responseText); - - var response = xmlhttp.responseXML.childNodes[0]; - - if (response.firstChild.tagName == 'error') { - _error(response.firstChild.firstChild.nodeValue); - } - - if (response.firstChild.tagName != 'reset') { - _error('Invalid response from server', xmlhttp.responseText); - } - - _syncInProgress = false; - - if (callback) { - callback(); - } - }); - } - - function resetClient() { Zotero.debug("Resetting client"); @@ -1706,7 +1672,6 @@ Zotero.Sync.Server = new function () { _error("Auto-syncing disabled until " + timeStr); } - if (firstChild.localName == 'error') { switch (firstChild.getAttribute('code')) { case 'INVALID_UPLOAD_DATA': @@ -1831,47 +1796,43 @@ Zotero.Sync.Server = new function () { } - function _checkServerSessionLock(errorNode, callback) { - var code = errorNode.getAttribute('code'); - if (code != 'SYNC_IN_PROGRESS') { - return false; - } - var lastAccessTime = errorNode.getAttribute('lastAccessTime'); - var relativeDateStr = Zotero.Date.toRelativeDate(lastAccessTime * 1000); - var ipAddress = errorNode.getAttribute('ipAddress'); + /** + * @private + * @param {DOMElement} response + * @param {Function} callback + */ + function _checkServerLock(response, callback) { + _checkTimer = null; - var pr = Components.classes["@mozilla.org/network/default-prompt;1"] - .createInstance(Components.interfaces.nsIPrompt); - var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING) - + (pr.BUTTON_POS_1) * (pr.BUTTON_TITLE_CANCEL) - + pr.BUTTON_POS_1_DEFAULT; - var index = pr.confirmEx( - Zotero.getString('general.warning'), - // TODO: localize - "Another sync operation, started " + relativeDateStr + " from " - + (ipAddress - ? "another IP address (" + ipAddress + ")" - : "the current IP address") - + ", " - + "is still in progress. " - + "This may indicate an active sync " - + (ipAddress ? "from another computer " : "") - + "or may have been caused by an interrupted session." - + "\n\n" - + "Do you want to reset the existing session? You should only do so " - + "if you do not believe another sync process is currently running.", - buttonFlags, - "Reset Session", - null, null, null, {} - ); + var mode; - if (index == 0) { - // TODO: - Zotero.Sync.Server.resetServer(callback); - return true; + switch (response.firstChild.localName) { + case 'queued': + mode = 'queued'; + break; + + case 'locked': + mode = 'locked'; + break; + + default: + return false; } - return false; + if (mode == 'queued') { + var msg = "Upload queued"; + } + else { + var msg = "Associated libraries are locked"; + } + + var wait = parseInt(response.firstChild.getAttribute('wait')); + if (!wait || isNaN(wait)) { + wait = 5000; + } + Zotero.debug(msg + " — waiting " + wait + "ms before next check"); + _checkTimer = setTimeout(function () { callback(mode); }, wait); + return true; } @@ -1891,8 +1852,7 @@ Zotero.Sync.Server = new function () { Zotero.DB.beginTransaction(); - var sql = "SELECT value FROM settings WHERE " - + "setting='account' AND key='username'"; + var sql = "SELECT value FROM settings WHERE setting='account' AND key='username'"; var lastUsername = Zotero.DB.valueQuery(sql); var username = Zotero.Sync.Server.username; var lastUserID = Zotero.userID; @@ -1972,6 +1932,7 @@ Zotero.Sync.Server = new function () { lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync'); } + Zotero.DB.commitTransaction(); return -1; } @@ -1991,10 +1952,10 @@ Zotero.Sync.Server = new function () { // Update userID in relations if (lastUserID && lastLibraryID) { Zotero.Relations.updateUser(lastUserID, lastLibraryID, userID, libraryID); + + Zotero.Sync.Server.resetClient(); + Zotero.Sync.Storage.resetAllSyncStates(); } - - Zotero.Sync.Server.resetClient(); - Zotero.Sync.Storage.resetAllSyncStates(); } if (lastUsername != username) { @@ -2097,10 +2058,6 @@ Zotero.Sync.Server = new function () { Zotero.reloadDataObjects(); Zotero.Sync.EventListener.resetIgnored(); - if (_sessionID && _sessionLock) { - Zotero.Sync.Server.unlock() - } - _callbacks.onError(e); throw (e); diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css index 58fba9cf5f..95122257d0 100644 --- a/chrome/skin/default/zotero/overlay.css +++ b/chrome/skin/default/zotero/overlay.css @@ -233,7 +233,7 @@ list-style-image: url(chrome://zotero/skin/arrow_rotate_animated.png); } -#zotero-tb-sync #zotero-last-sync-time +#zotero-tb-sync #zotero-tb-sync-status { color: gray; }