diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js
index 4ecd86eac0..a562ad2f84 100644
--- a/chrome/content/zotero/xpcom/attachments.js
+++ b/chrome/content/zotero/xpcom/attachments.js
@@ -921,20 +921,7 @@ Zotero.Attachments = new function () {
}
}
else {
- Zotero.debug("Saving file with saveURI()");
- const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
- var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
- .createInstance(nsIWBP);
- wbp.persistFlags = nsIWBP.PERSIST_FLAGS_FROM_CACHE;
- var ioService = Components.classes["@mozilla.org/network/io-service;1"]
- .getService(Components.interfaces.nsIIOService);
- var nsIURL = ioService.newURI(url, null, null);
- var deferred = Zotero.Promise.defer();
- wbp.progressListener = new Zotero.WebProgressFinishListener(function () {
- deferred.resolve();
- });
- Zotero.Utilities.Internal.saveURI(wbp, nsIURL, tmpFile);
- yield deferred.promise;
+ yield Zotero.HTTP.download(url, tmpFile);
}
var attachmentItem;
@@ -1100,20 +1087,18 @@ Zotero.Attachments = new function () {
let enforcingFileType = false;
try {
- await new Zotero.Promise(function (resolve) {
- var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
- .createInstance(Components.interfaces.nsIWebBrowserPersist);
- if (options.cookieSandbox) {
- options.cookieSandbox.attachToInterfaceRequestor(wbp);
+ let headers = {};
+ if (options.referrer) {
+ headers.Referer = options.referrer;
+ }
+ await Zotero.HTTP.download(
+ url,
+ path,
+ {
+ headers,
+ cookieSandbox: options.cookieSandbox
}
-
- wbp.progressListener = new Zotero.WebProgressFinishListener(() => resolve());
- var headers = {};
- if (options.referrer) {
- headers.Referer = options.referrer;
- }
- Zotero.Utilities.Internal.saveURI(wbp, url, path, headers);
- });
+ );
if (options.enforceFileType) {
enforcingFileType = true;
diff --git a/chrome/content/zotero/xpcom/file.js b/chrome/content/zotero/xpcom/file.js
index 3214474094..3427a6e2cc 100644
--- a/chrome/content/zotero/xpcom/file.js
+++ b/chrome/content/zotero/xpcom/file.js
@@ -448,16 +448,15 @@ Zotero.File = new function(){
this.download = async function (uri, path) {
var uriStr = uri.spec || uri;
+
const isHTTP = uriStr.startsWith('http');
+ if (uriStr.startsWith('http')) {
+ Zotero.warn("Zotero.File.download() is deprecated for HTTP(S) URLs -- use Zotero.HTTP.download()");
+ return Zotero.HTTP.download(uri, path);
+ }
Zotero.debug(`Saving ${uriStr} to ${path.pathQueryRef || path}`);
- if (isHTTP && Zotero.HTTP.browserIsOffline()) {
- let msg = `Download failed: ${Zotero.appName} is currently offline`;
- Zotero.debug(msg, 2);
- throw new Error(msg);
- }
-
var deferred = Zotero.Promise.defer();
const inputChannel = NetUtil.newChannel({
uri,
diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js
index 71db3d7680..e8b0b13fc8 100644
--- a/chrome/content/zotero/xpcom/http.js
+++ b/chrome/content/zotero/xpcom/http.js
@@ -3,6 +3,7 @@
* @namespace
*/
Zotero.HTTP = new function() {
+ this.disableErrorRetry = false;
var _errorDelayIntervals = [2500, 5000, 10000, 20000, 40000, 60000, 120000, 240000, 300000];
var _errorDelayMax = 60 * 60 * 1000; // 1 hour
@@ -169,7 +170,7 @@ Zotero.HTTP = new function() {
continue;
}
// Don't retry if errorDelayMax is 0
- if (options.errorDelayMax === 0) {
+ if (options.errorDelayMax === 0 || Zotero.HTTP.disableErrorRetry) {
throw e;
}
// Automatically retry other 5xx errors by default
@@ -269,7 +270,10 @@ Zotero.HTTP = new function() {
var deferred = Zotero.Promise.defer();
- if (!this.mock || url.startsWith('resource://') || url.startsWith('chrome://')) {
+ if (!this.mock
+ || options.noMock
+ || url.startsWith('resource://')
+ || url.startsWith('chrome://')) {
var xmlhttp = new XMLHttpRequest();
}
else {
@@ -324,11 +328,13 @@ Zotero.HTTP = new function() {
channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
}
- // Don't follow redirects
+ let notificationCallbacks = options.notificationCallbacks || {};
if (options.followRedirects === false) {
- channel.notificationCallbacks = {
- QueryInterface: ChromeUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIChannelEventSync]),
- getInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
+ if (notificationCallbacks.asyncOnChannelRedirect) {
+ throw new Error("Can't set asyncOnChannelRedirect and followRedirects = false");
+ }
+ notificationCallbacks = {
+ ...notificationCallbacks,
asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
redirectStatus = (flags & Ci.nsIChannelEventSink.REDIRECT_PERMANENT) ? 301 : 302;
redirectLocation = newChannel.URI.spec;
@@ -337,6 +343,9 @@ Zotero.HTTP = new function() {
}
};
}
+ if (notificationCallbacks) {
+ channel.notificationCallbacks = wrapNotificationCallbacks(notificationCallbacks);
+ }
}
// Set responseType
@@ -545,6 +554,61 @@ Zotero.HTTP = new function() {
return deferred.promise;
};
+
+ /**
+ * Create an nsIInterfaceRequestor object based on the callbacks provided
+ */
+ function wrapNotificationCallbacks(callbacks) {
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface(aIID) {
+ // Handle nsIProgressEventSink (for onProgress, onStatus)
+ if (aIID.equals(Ci.nsIProgressEventSink) && (callbacks.onProgress || callbacks.onStatus)) {
+ return {
+ onProgress: callbacks.onProgress || function () {},
+ onStatus: callbacks.onStatus || function () {},
+ };
+ }
+
+ // Handle nsIChannelEventSink (for asyncOnChannelRedirect)
+ if (aIID.equals(Ci.nsIChannelEventSink) && callbacks.asyncOnChannelRedirect) {
+ return {
+ asyncOnChannelRedirect: callbacks.asyncOnChannelRedirect,
+ };
+ }
+
+ throw Components.Exception("No interface available", Cr.NS_ERROR_NO_INTERFACE);
+ }
+ };
+ }
+
+
+ /**
+ * Download a file
+ *
+ * @param {nsIURI|String} url - URL to request
+ * @param {String} path - Path to save file to
+ * @param {Object} [options] - See `Zotero.HTTP.request()`
+ */
+ this.download = async function(uri, path, options = {}) {
+ // TODO: Convert request() to fetch() and use ReadableStream
+ var req = await this.request(
+ 'GET',
+ uri,
+ {
+ ...options,
+ responseType: 'blob',
+ // Downloads can have channel notification callbacks, etc., so always do them for real
+ noMock: true
+ }
+ );
+ var bytes = await IOUtils.write(path, await req.response.bytes());
+ Zotero.debug(`Saved file to ${path} (${bytes} byte${bytes != 1 ? 's' : ''})`);
+ return req;
+ };
+
+
/**
* Send an HTTP GET request via XMLHTTPRequest
*
@@ -1087,6 +1151,9 @@ Zotero.HTTP = new function() {
this.getDisplayURI = function (uri, noCredentials) {
+ if (typeof uri == 'string') {
+ uri = NetUtil.newURI(uri);
+ }
if (!uri.password) return uri;
uri = uri.mutate();
if (noCredentials) {
diff --git a/chrome/content/zotero/xpcom/storage/streamListener.js b/chrome/content/zotero/xpcom/storage/streamListener.js
deleted file mode 100644
index ba3f4ccddb..0000000000
--- a/chrome/content/zotero/xpcom/storage/streamListener.js
+++ /dev/null
@@ -1,276 +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 *****
-*/
-
-
-/**
- * Stream listener that can handle both download and upload requests
- *
- * Possible properties of data object:
- * - onStart: f(request)
- * - onProgress: f(request, progress, progressMax)
- * - onStop: f(request, status, response)
- * - onCancel: f(request, status)
- * - streams: array of streams to close on completion
- */
-Zotero.Sync.Storage.StreamListener = function (data) {
- this._data = data;
-}
-
-Zotero.Sync.Storage.StreamListener.prototype = {
- _channel: null,
-
- // nsIProgressEventSink
- onProgress: function (request, progress, progressMax) {
- Zotero.debug("onProgress with " + progress + "/" + progressMax);
- this._onProgress(request, progress, progressMax);
- },
-
- onStatus: function (request, status, statusArg) {
- Zotero.debug('onStatus with ' + status);
- },
-
- // nsIRequestObserver
- // Note: For uploads, this isn't called until data is done uploading
- onStartRequest: function (request) {
- Zotero.debug('onStartRequest');
- this._response = "";
-
- this._onStart(request);
- },
-
- onStopRequest: function (request, status) {
- Zotero.debug('onStopRequest with ' + status);
-
- // Some errors from https://developer.mozilla.org/en-US/docs/Table_Of_Errors
- var msg = "";
- switch (status) {
- // Normal
- case 0:
- break;
-
- // NS_BINDING_ABORTED
- case 0x804b0002:
- msg = "Request cancelled";
- break;
-
- // NS_ERROR_NET_INTERRUPT
- case 0x804B0047:
- msg = "Request interrupted";
- break;
-
- // NS_ERROR_NET_TIMEOUT
- case 0x804B000E:
- msg = "Request timed out";
- break;
-
- default:
- msg = "Request failed";
- break;
- }
-
- if (msg) {
- msg += " in Zotero.Sync.Storage.StreamListener.onStopRequest() (" + status + ")";
- Components.utils.reportError(msg);
- Zotero.debug(msg, 1);
- }
-
- this._onStop(request, status);
- },
-
- // nsIWebProgressListener
- onProgressChange: function (wp, request, curSelfProgress,
- maxSelfProgress, curTotalProgress, maxTotalProgress) {
- //Zotero.debug("onProgressChange with " + curTotalProgress + "/" + maxTotalProgress);
-
- // onProgress gets called too, so this isn't necessary
- //this._onProgress(request, curTotalProgress, maxTotalProgress);
- },
-
- onStateChange: function (wp, request, stateFlags, status) {
- Zotero.debug("onStateChange with " + stateFlags);
-
- if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_REQUEST) {
- if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_START) {
- this._onStart(request);
- }
- else if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) {
- this._onStop(request, status);
- }
- }
- },
-
- onStatusChange: function (progress, request, status, message) {
- Zotero.debug("onStatusChange with '" + message + "'");
- },
- onLocationChange: function () {
- Zotero.debug('onLocationChange');
- },
- onSecurityChange: function () {
- Zotero.debug('onSecurityChange');
- },
-
- // nsIStreamListener
- onDataAvailable: function (request, stream, sourceOffset, length) {
- Zotero.debug('onDataAvailable');
- var scriptableInputStream =
- Components.classes["@mozilla.org/scriptableinputstream;1"]
- .createInstance(Components.interfaces.nsIScriptableInputStream);
- scriptableInputStream.init(stream);
-
- var data = scriptableInputStream.read(length);
- Zotero.debug(data);
- this._response += data;
- },
-
- // nsIChannelEventSink
- //
- // If this._data.onChannelRedirect exists, it should return a promise resolving to true to
- // follow the redirect or false to cancel it
- onChannelRedirect: Zotero.Promise.coroutine(function* (oldChannel, newChannel, flags) {
- Zotero.debug('onChannelRedirect');
-
- if (this._data && this._data.onChannelRedirect) {
- let result = yield this._data.onChannelRedirect(oldChannel, newChannel, flags);
- if (!result) {
- oldChannel.cancel(Components.results.NS_BINDING_ABORTED);
- newChannel.cancel(Components.results.NS_BINDING_ABORTED);
- Zotero.debug("Cancelling redirect");
- // TODO: Prevent onStateChange error
- return false;
- }
- }
-
- // if redirecting, store the new channel
- this._channel = newChannel;
- }),
-
- asyncOnChannelRedirect: function (oldChan, newChan, flags, redirectCallback) {
- Zotero.debug('asyncOnRedirect');
-
- this.onChannelRedirect(oldChan, newChan, flags)
- .then(function (result) {
- redirectCallback.onRedirectVerifyCallback(
- result ? Components.results.NS_SUCCEEDED : Components.results.NS_FAILED
- );
- })
- .catch(function (e) {
- Zotero.logError(e);
- redirectCallback.onRedirectVerifyCallback(Components.results.NS_FAILED);
- });
- },
-
- // nsIHttpEventSink
- onRedirect: function (oldChannel, newChannel) {
- Zotero.debug('onRedirect');
-
- var newURL = Zotero.HTTP.getDisplayURI(newChannel.URI).spec;
- Zotero.debug("Redirecting to " + newURL);
- },
-
-
- //
- // Private methods
- //
- _onStart: function (request) {
- Zotero.debug('Starting request');
- if (this._data && this._data.onStart) {
- this._data.onStart(request);
- }
- },
-
- _onProgress: function (request, progress, progressMax) {
- if (this._data && this._data.onProgress) {
- this._data.onProgress(request, progress, progressMax);
- }
- },
-
- _onStop: function (request, status) {
- var cancelled = status == 0x804b0002; // NS_BINDING_ABORTED
-
- if (!cancelled && status == 0 && request instanceof Components.interfaces.nsIHttpChannel) {
- request.QueryInterface(Components.interfaces.nsIHttpChannel);
- try {
- status = request.responseStatus;
- }
- catch (e) {
- Zotero.debug("Request responseStatus not available", 1);
- status = 0;
- }
- Zotero.debug('Request ended with status code ' + status);
- request.QueryInterface(Components.interfaces.nsIRequest);
- }
- else {
- Zotero.debug('Request ended with status ' + status);
- status = 0;
- }
-
- if (this._data.streams) {
- for (let stream of this._data.streams) {
- stream.close();
- }
- }
-
- if (cancelled) {
- if (this._data.onCancel) {
- this._data.onCancel(request, status);
- }
- }
- else {
- if (this._data.onStop) {
- this._data.onStop(request, status, this._response);
- }
- }
-
- this._channel = null;
- },
-
- // nsIInterfaceRequestor
- getInterface: function (iid) {
- try {
- return this.QueryInterface(iid);
- }
- catch (e) {
- throw Components.results.NS_NOINTERFACE;
- }
- },
-
- QueryInterface: function(iid) {
- if (iid.equals(Components.interfaces.nsISupports) ||
- iid.equals(Components.interfaces.nsIInterfaceRequestor) ||
- iid.equals(Components.interfaces.nsIChannelEventSink) ||
- iid.equals(Components.interfaces.nsIProgressEventSink) ||
- iid.equals(Components.interfaces.nsIHttpEventSink) ||
- iid.equals(Components.interfaces.nsIStreamListener) ||
- iid.equals(Components.interfaces.nsIWebProgressListener)) {
- return this;
- }
- throw Components.results.NS_NOINTERFACE;
- },
-
- _safeSpec: function (uri) {
- return uri.scheme + '://' + uri.username + ':********@'
- + uri.hostPort + uri.pathQueryRef
- },
-};
diff --git a/chrome/content/zotero/xpcom/storage/webdav.js b/chrome/content/zotero/xpcom/storage/webdav.js
index 6f60a708cf..60448c5e60 100644
--- a/chrome/content/zotero/xpcom/storage/webdav.js
+++ b/chrome/content/zotero/xpcom/storage/webdav.js
@@ -278,8 +278,8 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
* @param {Zotero.Sync.Storage.Request} request
* @return {Promise}
*/
- downloadFile: Zotero.Promise.coroutine(function* (request) {
- yield this._init();
+ downloadFile: async function (request) {
+ await this._init();
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
if (!item) {
@@ -294,7 +294,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
}
// Retrieve modification time from server
- var metadata = yield this._getStorageFileMetadata(item, request);
+ var metadata = await this._getStorageFileMetadata(item, request);
if (!request.isRunning()) {
Zotero.debug("Download request '" + request.name
@@ -307,7 +307,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
return new Zotero.Sync.Storage.Result;
}
- var fileModTime = yield item.attachmentModificationTime;
+ var fileModTime = await item.attachmentModificationTime;
if (metadata.mtime == fileModTime) {
Zotero.debug("File mod time matches remote file -- skipping download of "
+ item.libraryKey);
@@ -315,10 +315,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
var updateItem = item.attachmentSyncState != 1
item.attachmentSyncedModificationTime = metadata.mtime;
item.attachmentSyncState = "in_sync";
- yield item.saveTx({ skipAll: true });
+ await item.saveTx({ skipAll: true });
// DEBUG: Necessary?
if (updateItem) {
- yield item.updateSynced(false);
+ await item.updateSynced(false);
}
return new Zotero.Sync.Storage.Result({
@@ -329,9 +329,8 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
var uri = this._getItemURI(item);
var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
- yield Zotero.File.removeIfExists(destPath);
+ await Zotero.File.removeIfExists(destPath);
- var deferred = Zotero.Promise.defer();
var requestData = {
item,
mtime: metadata.mtime,
@@ -339,88 +338,71 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
compressed: true
};
- var listener = new Zotero.Sync.Storage.StreamListener(
- {
- onStart: function (req) {
- if (request.isFinished()) {
- Zotero.debug("Download request " + request.name
- + " stopped before download started -- closing channel");
- req.cancel(0x804b0002); // NS_BINDING_ABORTED
- deferred.resolve(new Zotero.Sync.Storage.Result);
+ return new Promise(async (resolve, reject) => {
+ try {
+ let req = await Zotero.HTTP.download(
+ uri,
+ destPath,
+ {
+ successCodes: [200, 404],
+ noCache: true,
+ notificationCallbacks: {
+ onProgress: function (a, b, c) {
+ request.onProgress(a, b, c)
+ },
+ },
+ errorDelayIntervals: this.ERROR_DELAY_INTERVALS,
+ errorDelayMax: this.ERROR_DELAY_MAX,
}
- },
- onProgress: function (a, b, c) {
- request.onProgress(a, b, c)
- },
- onStop: Zotero.Promise.coroutine(function* (req, status, res) {
- request.setChannel(false);
-
- if (status == 404) {
- let msg = "Remote ZIP file not found for item " + item.libraryKey;
- Zotero.debug(msg, 2);
- Components.utils.reportError(msg);
-
- // Delete the orphaned prop file
- try {
- yield this._deleteStorageFiles([item.key + ".prop"]);
- }
- catch (e) {
- Zotero.logError(e);
- }
-
- deferred.resolve(new Zotero.Sync.Storage.Result);
- return;
- }
- else if (status != 200) {
- try {
- this._throwFriendlyError("GET", dispURL, status);
- }
- catch (e) {
- deferred.reject(e);
- }
- return;
- }
-
- // Don't try to process if the request has been cancelled
- if (request.isFinished()) {
- Zotero.debug("Download request " + request.name
- + " is no longer running after file download");
- deferred.resolve(new Zotero.Sync.Storage.Result);
- return;
- }
-
- Zotero.debug("Finished download of " + destPath);
+ );
+
+ if (req.status == 404) {
+ let msg = "Remote ZIP file not found for item " + item.libraryKey;
+ Zotero.debug(msg, 2);
+ Cu.reportError(msg);
+ // Delete the orphaned prop file
try {
- deferred.resolve(
- Zotero.Sync.Storage.Local.processDownload(requestData)
- );
+ await this._deleteStorageFiles([item.key + ".prop"]);
}
catch (e) {
- deferred.reject(e);
- }
- }.bind(this)),
- onCancel: function (req, status) {
- Zotero.debug("Request cancelled");
- if (deferred.promise.isPending()) {
- deferred.resolve(new Zotero.Sync.Storage.Result);
+ Zotero.logError(e);
}
+
+ resolve(new Zotero.Sync.Storage.Result);
+ return;
}
+
+ // Don't try to process if the request has been cancelled
+ if (request.isFinished()) {
+ Zotero.debug("Download request " + request.name
+ + " is no longer running after file download");
+ resolve(new Zotero.Sync.Storage.Result);
+ return;
+ }
+
+ Zotero.debug("Finished download of " + destPath);
+
+ resolve(
+ Zotero.Sync.Storage.Local.processDownload(requestData)
+ );
}
- );
-
- // Don't display password in console
- var dispURL = Zotero.HTTP.getDisplayURI(uri).spec;
- Zotero.debug('Saving ' + dispURL);
- const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
- var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
- .createInstance(nsIWBP);
- wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
- wbp.progressListener = listener;
- Zotero.Utilities.Internal.saveURI(wbp, uri, destPath);
-
- return deferred.promise;
- }),
+ catch (e) {
+ if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
+ try {
+ let dispURL = Zotero.HTTP.getDisplayURI(uri).spec;
+ this._throwFriendlyError("GET", dispURL, e.xmlhttp.status);
+ }
+ catch (e) {
+ reject(e);
+ }
+ return;
+ }
+ Zotero.logError(e);
+ reject(new Error(Zotero.Sync.Storage.defaultError));
+ }
+ });
+ },
uploadFile: Zotero.Promise.coroutine(function* (request) {
diff --git a/chrome/content/zotero/xpcom/storage/zfs.js b/chrome/content/zotero/xpcom/storage/zfs.js
index 9ab81fc369..c63a3b912b 100644
--- a/chrome/content/zotero/xpcom/storage/zfs.js
+++ b/chrome/content/zotero/xpcom/storage/zfs.js
@@ -49,7 +49,7 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
* @param {Zotero.Sync.Storage.Request} request
* @return {Promise}
*/
- downloadFile: Zotero.Promise.coroutine(function* (request) {
+ downloadFile: async function (request) {
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
if (!item) {
throw new Error("Item '" + request.name + "' not found");
@@ -63,195 +63,147 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
- // saveURI() below appears not to create empty files for Content-Length: 0,
- // so we create one here just in case, which also lets us check file access
+ // Create an empty file to check file access
try {
- yield IOUtils.write(destPath, new Uint8Array());
+ await IOUtils.write(destPath, new Uint8Array());
}
catch (e) {
Zotero.File.checkFileAccessError(e, destPath, 'create');
}
- var deferred = Zotero.Promise.defer();
var requestData = {item};
- var listener = new Zotero.Sync.Storage.StreamListener(
- {
- onStart: function (req) {
- if (request.isFinished()) {
- Zotero.debug("Download request " + request.name
- + " stopped before download started -- closing channel");
- req.cancel(Components.results.NS_BINDING_ABORTED);
- deferred.resolve(new Zotero.Sync.Storage.Result);
- }
- },
- onChannelRedirect: Zotero.Promise.coroutine(function* (oldChannel, newChannel, flags) {
- // These will be used in processDownload() if the download succeeds
- oldChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
-
- Zotero.debug("CHANNEL HERE FOR " + item.libraryKey + " WITH " + oldChannel.status);
- Zotero.debug(oldChannel.URI.spec);
- Zotero.debug(newChannel.URI.spec);
-
- var header;
- try {
- header = "Zotero-File-Modification-Time";
- requestData.mtime = parseInt(oldChannel.getResponseHeader(header));
- header = "Zotero-File-MD5";
- requestData.md5 = oldChannel.getResponseHeader(header);
- header = "Zotero-File-Compressed";
- requestData.compressed = oldChannel.getResponseHeader(header) == 'Yes';
- }
- catch (e) {
- deferred.reject(new Error(`${header} header not set in file request for ${item.libraryKey}`));
- return false;
- }
-
- if (!(yield OS.File.exists(path))) {
- return true;
- }
-
- var updateHash = false;
- var fileModTime = yield item.attachmentModificationTime;
- if (requestData.mtime == fileModTime) {
- Zotero.debug("File mod time matches remote file -- skipping download of "
- + item.libraryKey);
- }
- // If not compressed, check hash, in case only timestamp changed
- else if (!requestData.compressed && (yield item.attachmentHash) == requestData.md5) {
- Zotero.debug("File hash matches remote file -- skipping download of "
- + item.libraryKey);
- updateHash = true;
- }
- else {
- return true;
- }
-
- // Update local metadata and stop request, skipping file download
- yield OS.File.setDates(path, null, new Date(requestData.mtime));
- item.attachmentSyncedModificationTime = requestData.mtime;
- if (updateHash) {
- item.attachmentSyncedHash = requestData.md5;
- }
- item.attachmentSyncState = "in_sync";
- yield item.saveTx({ skipAll: true });
-
- deferred.resolve(new Zotero.Sync.Storage.Result({
- localChanges: true
- }));
-
- return false;
- }),
- onProgress: function (req, progress, progressMax) {
- request.onProgress(progress, progressMax);
- },
- onStop: function (req, status, res) {
- request.setChannel(false);
-
- if (status != 200) {
- if (status == 404) {
- Zotero.debug("Remote file not found for item " + item.libraryKey);
- // Don't refresh item pane rows when nothing happened
- request.skipProgressBarUpdate = true;
- deferred.resolve(new Zotero.Sync.Storage.Result);
- return;
- }
-
- // Check for SSL certificate error
- if (status == 0) {
- try {
- Zotero.HTTP.checkSecurity(req);
- }
- catch (e) {
- deferred.reject(e);
- return;
- }
- }
-
- // If S3 connection is interrupted, delay and retry, or bail if too many
- // consecutive failures
- if (status == 0 || status == 500 || status == 503) {
- if (++this._s3ConsecutiveFailures < this._maxS3ConsecutiveFailures) {
- let libraryKey = item.libraryKey;
- let msg = "S3 returned 0 for " + libraryKey + " -- retrying download"
- Components.utils.reportError(msg);
- Zotero.debug(msg, 1);
- if (this._s3Backoff < this._maxS3Backoff) {
- this._s3Backoff *= 2;
- }
- Zotero.debug("Delaying " + libraryKey + " download for "
- + this._s3Backoff + " seconds", 2);
- Zotero.Promise.delay(this._s3Backoff * 1000)
- .then(function () {
- deferred.resolve(this.downloadFile(request));
- }.bind(this));
- return;
- }
-
- Zotero.debug(this._s3ConsecutiveFailures
- + " consecutive S3 failures -- aborting", 1);
- this._s3ConsecutiveFailures = 0;
- }
-
- var msg = "Unexpected status code " + status + " for GET " + uri;
- Zotero.debug(msg, 1);
- Components.utils.reportError(msg);
- // Output saved content, in case an error was captured
- try {
- let sample = Zotero.File.getContents(destPath, null, 4096);
- if (sample) {
- Zotero.debug(sample, 1);
- }
- }
- catch (e) {
- Zotero.debug(e, 1);
- }
- deferred.reject(new Error(Zotero.Sync.Storage.defaultError));
- return;
- }
-
- // Don't try to process if the request has been cancelled
- if (request.isFinished()) {
- Zotero.debug("Download request " + request.name
- + " is no longer running after file download", 2);
- deferred.resolve(new Zotero.Sync.Storage.Result);
- return;
- }
-
- Zotero.debug("Finished download of " + destPath);
-
- try {
- deferred.resolve(
- Zotero.Sync.Storage.Local.processDownload(requestData)
- );
- }
- catch (e) {
- deferred.reject(e);
- }
- }.bind(this),
- onCancel: function (req, status) {
- Zotero.debug("Request cancelled");
- if (deferred.promise.isPending()) {
- deferred.resolve(new Zotero.Sync.Storage.Result);
- }
- }
- }
- );
-
var params = this._getRequestParams(item.libraryID, `items/${item.key}/file`);
var uri = this.apiClient.buildRequestURI(params);
- var headers = this.apiClient.getHeaders();
- Zotero.debug('Saving ' + uri);
- const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
- var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
- .createInstance(nsIWBP);
- wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
- wbp.progressListener = listener;
- Zotero.Utilities.Internal.saveURI(wbp, uri, destPath, headers);
-
- return deferred.promise;
- }),
+ return new Promise(async (resolve, reject) => {
+ try {
+ let req = await Zotero.HTTP.download(
+ uri,
+ destPath,
+ {
+ successCodes: [200, 404],
+ headers: this.apiClient.getHeaders(),
+ noCache: true,
+ notificationCallbacks: {
+ asyncOnChannelRedirect: async function (oldChannel, newChannel, flags, callback) {
+ // These will be used in processDownload() if the download succeeds
+ oldChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
+
+ Zotero.debug(`Handling ${oldChannel.responseStatus} redirect for ${item.libraryKey}`);
+ Zotero.debug(oldChannel.URI.spec);
+ Zotero.debug(newChannel.URI.spec);
+
+ var header;
+ try {
+ header = "Zotero-File-Modification-Time";
+ requestData.mtime = parseInt(oldChannel.getResponseHeader(header));
+ header = "Zotero-File-MD5";
+ requestData.md5 = oldChannel.getResponseHeader(header);
+ header = "Zotero-File-Compressed";
+ requestData.compressed = oldChannel.getResponseHeader(header) == 'Yes';
+ }
+ catch (_e) {
+ reject(new Error(`${header} header not set in file request for ${item.libraryKey}`));
+ callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
+ return;
+ }
+
+ if (!(await IOUtils.exists(path))) {
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ return;
+ }
+
+ var updateHash = false;
+ var fileModTime = await item.attachmentModificationTime;
+ if (requestData.mtime == fileModTime) {
+ Zotero.debug("File mod time matches remote file -- skipping download of "
+ + item.libraryKey);
+ }
+ // If not compressed, check hash, in case only timestamp changed
+ else if (!requestData.compressed && (await item.attachmentHash) == requestData.md5) {
+ Zotero.debug("File hash matches remote file -- skipping download of "
+ + item.libraryKey);
+ updateHash = true;
+ }
+ else {
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ return;
+ }
+
+ // Update local metadata and stop request, skipping file download
+ await OS.File.setDates(path, null, new Date(requestData.mtime));
+ item.attachmentSyncedModificationTime = requestData.mtime;
+ if (updateHash) {
+ item.attachmentSyncedHash = requestData.md5;
+ }
+ item.attachmentSyncState = "in_sync";
+ await item.saveTx({ skipAll: true });
+
+ resolve(new Zotero.Sync.Storage.Result({
+ localChanges: true
+ }));
+
+ callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
+ },
+
+ onProgress: function (req, progress, progressMax) {
+ request.onProgress(progress, progressMax);
+ },
+ },
+ }
+ );
+
+ if (req.status == 404) {
+ Zotero.debug("Remote file not found for item " + item.libraryKey);
+ // Don't refresh item pane rows when nothing happened
+ request.skipProgressBarUpdate = true;
+ resolve(new Zotero.Sync.Storage.Result);
+ return;
+ }
+
+ // Don't try to process if the request has been cancelled
+ if (request.isFinished()) {
+ Zotero.debug(`Download request ${request.name} is no longer running after file download`, 2);
+ resolve(new Zotero.Sync.Storage.Result);
+ return;
+ }
+
+ Zotero.debug("Finished download of " + destPath);
+
+ resolve(await Zotero.Sync.Storage.Local.processDownload(requestData));
+ }
+ catch (e) {
+ if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
+ // If S3 connection is interrupted, delay and retry, or bail if too many
+ // consecutive failures
+ if (e.xmlhttp.status == 0) {
+ if (++this._s3ConsecutiveFailures < this._maxS3ConsecutiveFailures) {
+ let libraryKey = item.libraryKey;
+ let msg = "S3 returned 0 for " + libraryKey + " -- retrying download";
+ Zotero.logError(msg);
+ if (this._s3Backoff < this._maxS3Backoff) {
+ this._s3Backoff *= 2;
+ }
+ Zotero.debug("Delaying " + libraryKey + " download for "
+ + this._s3Backoff + " seconds", 2);
+ Zotero.Promise.delay(this._s3Backoff * 1000)
+ .then(function () {
+ resolve(this.downloadFile(request));
+ }.bind(this));
+ return;
+ }
+
+ Zotero.debug(this._s3ConsecutiveFailures
+ + " consecutive S3 failures -- aborting", 1);
+ this._s3ConsecutiveFailures = 0;
+ }
+ }
+ Zotero.logError(e);
+ reject(new Error(Zotero.Sync.Storage.defaultError));
+ }
+ });
+ },
uploadFile: Zotero.Promise.coroutine(function* (request) {
diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js
index 650c3c8f84..d3399df154 100644
--- a/chrome/content/zotero/xpcom/utilities_internal.js
+++ b/chrome/content/zotero/xpcom/utilities_internal.js
@@ -459,6 +459,8 @@ Zotero.Utilities.Internal = {
* @param {Zotero.CookieSandbox} [cookieSandbox]
*/
saveURI: function (wbp, uri, target, headers, cookieSandbox) {
+ Zotero.warn("Zotero.Utilities.Internal.saveURI() is deprecated -- use Zotero.HTTP.download()");
+
// Handle gzip encoding
wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
// If not explicitly using cache, skip it
diff --git a/chrome/content/zotero/zotero.mjs b/chrome/content/zotero/zotero.mjs
index 8bb5d18f2f..4ca212e618 100644
--- a/chrome/content/zotero/zotero.mjs
+++ b/chrome/content/zotero/zotero.mjs
@@ -144,7 +144,6 @@ const xpcomFilesLocal = [
'storage/storageRequest',
'storage/storageResult',
'storage/storageUtilities',
- 'storage/streamListener',
'storage/zfs',
'storage/webdav',
'syncedSettings',
diff --git a/test/tests/zfsTest.js b/test/tests/zfsTest.js
index 0d8c5156be..178fe06721 100644
--- a/test/tests/zfsTest.js
+++ b/test/tests/zfsTest.js
@@ -103,6 +103,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
httpd.stop(() => defer.resolve());
yield defer.promise;
win.close();
+ Zotero.HTTP.disableErrorRetry = false;
})
after(function* () {
@@ -211,6 +212,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
item.attachmentSyncState = "to_download";
yield item.saveTx();
+ Zotero.HTTP.disableErrorRetry = true;
httpd.registerPathHandler(
`/users/1/items/${item.key}/file`,
{