diff --git a/chrome/content/zotero-platform/mac/overlay.css b/chrome/content/zotero-platform/mac/overlay.css index 66125bd2e..eb0910aa5 100644 --- a/chrome/content/zotero-platform/mac/overlay.css +++ b/chrome/content/zotero-platform/mac/overlay.css @@ -112,6 +112,7 @@ max-width: 28px; } + #zotero-tb-sync-stop .toolbarbutton-icon, #zotero-tb-sync-error .toolbarbutton-icon { width: 16px; } diff --git a/chrome/content/zotero/xpcom/storage/storageEngine.js b/chrome/content/zotero/xpcom/storage/storageEngine.js index 51792746b..87a6ba38f 100644 --- a/chrome/content/zotero/xpcom/storage/storageEngine.js +++ b/chrome/content/zotero/xpcom/storage/storageEngine.js @@ -282,6 +282,7 @@ Zotero.Sync.Storage.Engine.prototype.start = Zotero.Promise.coroutine(function* Zotero.Sync.Storage.Engine.prototype.stop = function () { + Zotero.debug("Stopping file syncing for " + this.library.name); for (let type in this.queues) { this.queues[type].stop(); } diff --git a/chrome/content/zotero/xpcom/sync/syncEngine.js b/chrome/content/zotero/xpcom/sync/syncEngine.js index bbe772568..036ab67a4 100644 --- a/chrome/content/zotero/xpcom/sync/syncEngine.js +++ b/chrome/content/zotero/xpcom/sync/syncEngine.js @@ -49,7 +49,6 @@ Zotero.Sync.Data.Engine = function (options) { this.libraryID = options.libraryID; this.library = Zotero.Libraries.get(options.libraryID); this.libraryTypeID = this.library.libraryTypeID; - this.requests = []; this.uploadBatchSize = 25; this.uploadDeletionBatchSize = 50; @@ -93,6 +92,8 @@ Zotero.Sync.Data.Engine.prototype.start = Zotero.Promise.coroutine(function* () this.libraryTypeID = info.userID; } + this._statusCheck(); + // Check if we've synced this library with the current architecture yet var libraryVersion = this.library.libraryVersion; if (!libraryVersion || libraryVersion == -1) { @@ -101,6 +102,8 @@ Zotero.Sync.Data.Engine.prototype.start = Zotero.Promise.coroutine(function* () libraryVersion = this.library.libraryVersion; } + this._statusCheck(); + // Perform a full sync if necessary, passing the getVersions() results if available. // // The full-sync flag (libraryID == -1) is set at the end of a successful upgrade, so this @@ -120,6 +123,8 @@ Zotero.Sync.Data.Engine.prototype.start = Zotero.Promise.coroutine(function* () sync: while (true) { + this._statusCheck(); + let downloadResult, uploadResult; try { @@ -199,17 +204,11 @@ Zotero.Sync.Data.Engine.prototype.start = Zotero.Promise.coroutine(function* () /** - * Stop all active requests - * - * @return {Promise} Promise from Zotero.Promise.settle() + * Stop the sync process */ Zotero.Sync.Data.Engine.prototype.stop = function () { - var funcs; - var request; - while (request = this.requests.shift()) { - funcs.push(() => request.stop()); - } - return Zotero.Promise.settle(funcs); + Zotero.debug("Stopping sync for " + this.library.name); + this._stopping = true; } @@ -254,7 +253,7 @@ Zotero.Sync.Data.Engine.prototype._startDownload = Zotero.Promise.coroutine(func // Get other object types // for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(this.libraryID)) { - this._failedCheck(); + this._statusCheck(); // For items, fetch top-level items first // @@ -496,7 +495,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType, keys.forEach(key => objectData[key] = null); while (true) { - this._failedCheck(); + this._statusCheck(); // Get data we've downloaded in a previous loop but failed to process var json = []; @@ -537,7 +536,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType, await Zotero.Promise.map( json, async function (batch) { - this._failedCheck(); + this._statusCheck(); Zotero.debug(`Processing batch of downloaded ${objectTypePlural} in ${this.library.name}`); @@ -559,6 +558,10 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType, this._getOptions({ onObjectProcessed: () => { num++; + // Check for stop every 5 items + if (num % 5 == 0) { + this._statusCheck(); + } }, // Increase the notifier batch size as we go, so that new items start coming in // one by one but then switch to larger chunks @@ -616,7 +619,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType, await this._restoreRestoredCollectionItems(restored); } - this._failedCheck(); + this._statusCheck(); // If all requests were successful, such that we had a chance to see all keys, remove keys we // didn't see from the sync queue so they don't keep being retried forever @@ -665,6 +668,8 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType, // Show conflict resolution window if (conflicts.length) { + this._statusCheck(); + let results = await Zotero.Sync.Data.Local.processConflicts( objectType, this.libraryID, conflicts, this._getOptions() ); @@ -827,6 +832,8 @@ Zotero.Sync.Data.Engine.prototype._downloadDeletions = Zotero.Promise.coroutine( } if (conflicts.length) { + this._statusCheck(); + // Sort conflicts by Date Modified conflicts.sort(function (a, b) { var d1 = a.left.dateModified; @@ -936,6 +943,8 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi // Get unsynced local objects for each object type for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(this.libraryID)) { + this._statusCheck(); + let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); @@ -1015,6 +1024,8 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi try { Zotero.debug(JSON.stringify(objectIDs)); for (let objectType in objectIDs) { + this._statusCheck(); + libraryVersion = yield this._uploadObjects( objectType, objectIDs[objectType], libraryVersion ); @@ -1022,6 +1033,8 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi Zotero.debug(JSON.stringify(objectDeletions)); for (let objectType in objectDeletions) { + this._statusCheck(); + libraryVersion = yield this._uploadDeletions( objectType, objectDeletions[objectType], libraryVersion ); @@ -1562,7 +1575,7 @@ Zotero.Sync.Data.Engine.prototype._fullSync = Zotero.Promise.coroutine(function* // Get object types for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(this.libraryID)) { - this._failedCheck(); + this._statusCheck(); let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); let ObjectType = Zotero.Utilities.capitalize(objectType); @@ -1774,6 +1787,19 @@ Zotero.Sync.Data.Engine.prototype._checkObjectUploadError = Zotero.Promise.corou }); +Zotero.Sync.Data.Engine.prototype._statusCheck = function () { + this._stopCheck(); + this._failedCheck(); +} + + +Zotero.Sync.Data.Engine.prototype._stopCheck = function () { + if (!this._stopping) return; + Zotero.debug("Sync stopped for " + this.library.name); + throw new Zotero.Sync.UserCancelledException; +} + + Zotero.Sync.Data.Engine.prototype._failedCheck = function () { if (this.stopOnError && this.failed) { Zotero.logError("Stopping on error"); diff --git a/chrome/content/zotero/xpcom/sync/syncFullTextEngine.js b/chrome/content/zotero/xpcom/sync/syncFullTextEngine.js index 23cc98dfc..3d1b9df20 100644 --- a/chrome/content/zotero/xpcom/sync/syncFullTextEngine.js +++ b/chrome/content/zotero/xpcom/sync/syncFullTextEngine.js @@ -44,7 +44,7 @@ Zotero.Sync.Data.FullTextEngine = function (options) { this.setStatus = options.setStatus || function () {}; this.onError = options.onError || function (e) {}; this.stopOnError = options.stopOnError; - this.requestPromises = []; + this._stopping = false; this.failed = false; } @@ -61,6 +61,8 @@ Zotero.Sync.Data.FullTextEngine.prototype.start = Zotero.Promise.coroutine(funct Zotero.debug("Library version hasn't changed -- skipping full-text download"); } + this._stopCheck(); + yield this._upload(); }) @@ -95,22 +97,26 @@ Zotero.Sync.Data.FullTextEngine.prototype._download = Zotero.Promise.coroutine(f } this.requestPromises = []; - for (let key of keys) { - // https://bugzilla.mozilla.org/show_bug.cgi?id=449811 - let tmpKey = key; - this.requestPromises.push( + + yield Zotero.Promise.map( + keys, + (key) => { + this._stopCheck(); this.apiClient.getFullTextForItem( this.library.libraryType, this.library.libraryTypeID, key ) - .then(function (results) { + .then((results) => { + this._stopCheck(); if (!results) return; return Zotero.Fulltext.setItemContent( - this.libraryID, tmpKey, results.data, results.version + this.libraryID, key, results.data, results.version ) - }.bind(this)) - ); - } - yield Zotero.Promise.all(this.requestPromises); + }) + }, + // Prepare twice the number of concurrent requests + { concurrency: 8 } + ); + yield Zotero.FullText.setLibraryVersion(this.libraryID, results.libraryVersion); }); @@ -125,6 +131,8 @@ Zotero.Sync.Data.FullTextEngine.prototype._upload = Zotero.Promise.coroutine(fun let lastItemID = 0; while (true) { + this._stopCheck(); + let objs = yield Zotero.FullText.getUnsyncedContent(this.libraryID, { maxSize: this.MAX_BATCH_SIZE, maxItems: this.MAX_BATCH_ITEMS, @@ -190,6 +198,13 @@ Zotero.Sync.Data.FullTextEngine.prototype._upload = Zotero.Promise.coroutine(fun Zotero.Sync.Data.FullTextEngine.prototype.stop = Zotero.Promise.coroutine(function* () { - // TODO: Cancel requests - throw new Error("Unimplemented"); + // TODO: Cancel requests? + this._stopping = true; }) + + +Zotero.Sync.Data.FullTextEngine.prototype._stopCheck = function () { + if (!this._stopping) return; + Zotero.debug("Full-text sync stopped for " + this.library.name); + throw new Zotero.Sync.UserCancelledException; +} diff --git a/chrome/content/zotero/xpcom/sync/syncRunner.js b/chrome/content/zotero/xpcom/sync/syncRunner.js index 8ad35e803..f0366c614 100644 --- a/chrome/content/zotero/xpcom/sync/syncRunner.js +++ b/chrome/content/zotero/xpcom/sync/syncRunner.js @@ -68,10 +68,10 @@ Zotero.Sync.Runner_Module = function (options = {}) { var _autoSyncTimer; var _firstInSession = true; var _syncInProgress = false; + var _stopping = false; var _manualSyncRequired = false; // TODO: make public? - var _syncEngines = []; - var _storageEngines = []; + var _currentEngine = null; var _storageControllers = {}; var _lastSyncStatus; @@ -117,6 +117,7 @@ Zotero.Sync.Runner_Module = function (options = {}) { return false; } _syncInProgress = true; + _stopping = false; yield Zotero.Notifier.trigger('start', 'sync', []); @@ -139,9 +140,10 @@ Zotero.Sync.Runner_Module = function (options = {}) { let client = this.getAPIClient({ apiKey }); let keyInfo = yield this.checkAccess(client, options); + _stopCheck(); + let emptyLibraryContinue = yield this.checkEmptyLibrary(keyInfo); if (!emptyLibraryContinue) { - yield this.end(options); Zotero.debug("Syncing cancelled because user library is empty"); return false; } @@ -150,7 +152,6 @@ Zotero.Sync.Runner_Module = function (options = {}) { .getService(Components.interfaces.nsIWindowMediator); let lastWin = wm.getMostRecentWindow("navigator:browser"); if (!(yield Zotero.Sync.Data.Local.checkUser(lastWin, keyInfo.userID, keyInfo.username))) { - yield this.end(options); Zotero.debug("User cancelled sync on username mismatch"); return false; } @@ -179,6 +180,8 @@ Zotero.Sync.Runner_Module = function (options = {}) { options.libraries ? Array.from(options.libraries) : [] ); + _stopCheck(); + // If items not yet loaded for libraries we need, load them now for (let libraryID of librariesToSync) { let library = Zotero.Libraries.get(libraryID); @@ -187,10 +190,14 @@ Zotero.Sync.Runner_Module = function (options = {}) { } } + _stopCheck(); + // Sync data and files, and then repeat if necessary let attempt = 1; let successfulLibraries = new Set(librariesToSync); while (librariesToSync.length) { + _stopCheck(); + if (attempt > 3) { // TODO: Back off and/or nicer error throw new Error("Too many sync attempts -- stopping"); @@ -201,6 +208,8 @@ Zotero.Sync.Runner_Module = function (options = {}) { successfulLibraries.delete(libraryID); }); + _stopCheck(); + // Run file sync on all libraries that passed the last data sync librariesToSync = yield _doFileSync(nextLibraries, engineOptions); if (librariesToSync.length) { @@ -208,6 +217,8 @@ Zotero.Sync.Runner_Module = function (options = {}) { continue; } + _stopCheck(); + // Run full-text sync on all libraries that haven't failed a data sync librariesToSync = yield _doFullTextSync([...successfulLibraries], engineOptions); if (librariesToSync.length) { @@ -550,13 +561,15 @@ Zotero.Sync.Runner_Module = function (options = {}) { var _doDataSync = Zotero.Promise.coroutine(function* (libraries, options, skipUpdateLastSyncTime) { var successfulLibraries = []; for (let libraryID of libraries) { + _stopCheck(); try { let opts = {}; Object.assign(opts, options); opts.libraryID = libraryID; - let engine = new Zotero.Sync.Data.Engine(opts); - yield engine.start(); + _currentEngine = new Zotero.Sync.Data.Engine(opts); + yield _currentEngine.start(); + _currentEngine = null; successfulLibraries.push(libraryID); } catch (e) { @@ -597,6 +610,7 @@ Zotero.Sync.Runner_Module = function (options = {}) { this.setSyncStatus(Zotero.getString('sync.status.syncingFiles')); var resyncLibraries = [] for (let libraryID of libraries) { + _stopCheck(); try { let opts = {}; Object.assign(opts, options); @@ -611,8 +625,9 @@ Zotero.Sync.Runner_Module = function (options = {}) { throw new Error("Too many file sync attempts for library " + libraryID); } tries--; - let engine = new Zotero.Sync.Storage.Engine(opts); - let results = yield engine.start(); + _currentEngine = new Zotero.Sync.Storage.Engine(opts); + let results = yield _currentEngine.start(); + _currentEngine = null; if (results.syncRequired) { resyncLibraries.push(libraryID); } @@ -624,6 +639,15 @@ Zotero.Sync.Runner_Module = function (options = {}) { } } catch (e) { + if (e instanceof Zotero.Sync.UserCancelledException) { + if (e.advanceToNextLibrary) { + Zotero.debug("Storage sync cancelled for library " + libraryID + " -- " + + "advancing to next library"); + continue; + } + throw e; + } + Zotero.debug("File sync failed for library " + libraryID); Zotero.logError(e); this.checkError(e); @@ -652,15 +676,21 @@ Zotero.Sync.Runner_Module = function (options = {}) { this.setSyncStatus(Zotero.getString('sync.status.syncingFullText')); var resyncLibraries = []; for (let libraryID of libraries) { + _stopCheck(); try { let opts = {}; Object.assign(opts, options); opts.libraryID = libraryID; - let engine = new Zotero.Sync.Data.FullTextEngine(opts); - yield engine.start(); + _currentEngine = new Zotero.Sync.Data.FullTextEngine(opts); + yield _currentEngine.start(); + _currentEngine = null; } catch (e) { + if (e instanceof Zotero.Sync.UserCancelledException) { + throw e; + } + if (e instanceof Zotero.HTTP.UnexpectedStatusException && e.status == 412) { resyncLibraries.push(libraryID); continue; @@ -762,8 +792,11 @@ Zotero.Sync.Runner_Module = function (options = {}) { this.stop = function () { - _syncEngines.forEach(engine => engine.stop()); - _storageEngines.forEach(engine => engine.stop()); + this.setSyncStatus(Zotero.getString('sync.stopping')); + _stopping = true; + if (_currentEngine) { + _currentEngine.stop(); + } } @@ -1143,14 +1176,17 @@ Zotero.Sync.Runner_Module = function (options = {}) { // Update sync icon var syncIcon = doc.getElementById('zotero-tb-sync'); + var stopIcon = doc.getElementById('zotero-tb-sync-stop'); if (state == 'animate') { syncIcon.setAttribute('status', state); // Disable button while spinning syncIcon.disabled = true; + stopIcon.hidden = false; } else { syncIcon.removeAttribute('status'); syncIcon.disabled = false; + stopIcon.hidden = true; } } @@ -1386,4 +1422,11 @@ Zotero.Sync.Runner_Module = function (options = {}) { // Set as .apiKey on Runner in tests or set in login manager return _apiKey || Zotero.Sync.Data.Local.getAPIKey() }) + + + function _stopCheck() { + if (_stopping) { + throw new Zotero.Sync.UserCancelledException; + } + } } diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul index 99a6759ab..de2c4c9ab 100644 --- a/chrome/content/zotero/zoteroPane.xul +++ b/chrome/content/zotero/zoteroPane.xul @@ -210,6 +210,10 @@ +