michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * This file includes the following constructors and global objects: michael@0: * michael@0: * Download michael@0: * Represents a single download, with associated state and actions. This object michael@0: * is transient, though it can be included in a DownloadList so that it can be michael@0: * managed by the user interface and persisted across sessions. michael@0: * michael@0: * DownloadSource michael@0: * Represents the source of a download, for example a document or an URI. michael@0: * michael@0: * DownloadTarget michael@0: * Represents the target of a download, for example a file in the global michael@0: * downloads directory, or a file in the system temporary directory. michael@0: * michael@0: * DownloadError michael@0: * Provides detailed information about a download failure. michael@0: * michael@0: * DownloadSaver michael@0: * Template for an object that actually transfers the data for the download. michael@0: * michael@0: * DownloadCopySaver michael@0: * Saver object that simply copies the entire source file to the target. michael@0: * michael@0: * DownloadLegacySaver michael@0: * Saver object that integrates with the legacy nsITransfer interface. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "Download", michael@0: "DownloadSource", michael@0: "DownloadTarget", michael@0: "DownloadError", michael@0: "DownloadSaver", michael@0: "DownloadCopySaver", michael@0: "DownloadLegacySaver", michael@0: ]; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration", michael@0: "resource://gre/modules/DownloadIntegration.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", michael@0: "resource://gre/modules/FileUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm") michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Services", michael@0: "resource://gre/modules/Services.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory", michael@0: "@mozilla.org/browser/download-history;1", michael@0: Ci.nsIDownloadHistory); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher", michael@0: "@mozilla.org/uriloader/external-helper-app-service;1", michael@0: Ci.nsPIExternalAppLauncher); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService", michael@0: "@mozilla.org/uriloader/external-helper-app-service;1", michael@0: Ci.nsIExternalHelperAppService); michael@0: michael@0: const BackgroundFileSaverStreamListener = Components.Constructor( michael@0: "@mozilla.org/network/background-file-saver;1?mode=streamlistener", michael@0: "nsIBackgroundFileSaver"); michael@0: michael@0: /** michael@0: * Returns true if the given value is a primitive string or a String object. michael@0: */ michael@0: function isString(aValue) { michael@0: // We cannot use the "instanceof" operator reliably across module boundaries. michael@0: return (typeof aValue == "string") || michael@0: (typeof aValue == "object" && "charAt" in aValue); michael@0: } michael@0: michael@0: /** michael@0: * Serialize the unknown properties of aObject into aSerializable. michael@0: */ michael@0: function serializeUnknownProperties(aObject, aSerializable) michael@0: { michael@0: if (aObject._unknownProperties) { michael@0: for (let property in aObject._unknownProperties) { michael@0: aSerializable[property] = aObject._unknownProperties[property]; michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Check for any unknown properties in aSerializable and preserve those in the michael@0: * _unknownProperties field of aObject. aFilterFn is called for each property michael@0: * name of aObject and should return true only for unknown properties. michael@0: */ michael@0: function deserializeUnknownProperties(aObject, aSerializable, aFilterFn) michael@0: { michael@0: for (let property in aSerializable) { michael@0: if (aFilterFn(property)) { michael@0: if (!aObject._unknownProperties) { michael@0: aObject._unknownProperties = { }; michael@0: } michael@0: michael@0: aObject._unknownProperties[property] = aSerializable[property]; michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * This determines the minimum time interval between updates to the number of michael@0: * bytes transferred, and is a limiting factor to the sequence of readings used michael@0: * in calculating the speed of the download. michael@0: */ michael@0: const kProgressUpdateIntervalMs = 400; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Download michael@0: michael@0: /** michael@0: * Represents a single download, with associated state and actions. This object michael@0: * is transient, though it can be included in a DownloadList so that it can be michael@0: * managed by the user interface and persisted across sessions. michael@0: */ michael@0: this.Download = function () michael@0: { michael@0: this._deferSucceeded = Promise.defer(); michael@0: } michael@0: michael@0: this.Download.prototype = { michael@0: /** michael@0: * DownloadSource object associated with this download. michael@0: */ michael@0: source: null, michael@0: michael@0: /** michael@0: * DownloadTarget object associated with this download. michael@0: */ michael@0: target: null, michael@0: michael@0: /** michael@0: * DownloadSaver object associated with this download. michael@0: */ michael@0: saver: null, michael@0: michael@0: /** michael@0: * Indicates that the download never started, has been completed successfully, michael@0: * failed, or has been canceled. This property becomes false when a download michael@0: * is started for the first time, or when a failed or canceled download is michael@0: * restarted. michael@0: */ michael@0: stopped: true, michael@0: michael@0: /** michael@0: * Indicates that the download has been completed successfully. michael@0: */ michael@0: succeeded: false, michael@0: michael@0: /** michael@0: * Indicates that the download has been canceled. This property can become michael@0: * true, then it can be reset to false when a canceled download is restarted. michael@0: * michael@0: * This property becomes true as soon as the "cancel" method is called, though michael@0: * the "stopped" property might remain false until the cancellation request michael@0: * has been processed. Temporary files or part files may still exist even if michael@0: * they are expected to be deleted, until the "stopped" property becomes true. michael@0: */ michael@0: canceled: false, michael@0: michael@0: /** michael@0: * When the download fails, this is set to a DownloadError instance indicating michael@0: * the cause of the failure. If the download has been completed successfully michael@0: * or has been canceled, this property is null. This property is reset to michael@0: * null when a failed download is restarted. michael@0: */ michael@0: error: null, michael@0: michael@0: /** michael@0: * Indicates the start time of the download. When the download starts, michael@0: * this property is set to a valid Date object. The default value is null michael@0: * before the download starts. michael@0: */ michael@0: startTime: null, michael@0: michael@0: /** michael@0: * Indicates whether this download's "progress" property is able to report michael@0: * partial progress while the download proceeds, and whether the value in michael@0: * totalBytes is relevant. This depends on the saver and the download source. michael@0: */ michael@0: hasProgress: false, michael@0: michael@0: /** michael@0: * Progress percent, from 0 to 100. Intermediate values are reported only if michael@0: * hasProgress is true. michael@0: * michael@0: * @note You shouldn't rely on this property being equal to 100 to determine michael@0: * whether the download is completed. You should use the individual michael@0: * state properties instead. michael@0: */ michael@0: progress: 0, michael@0: michael@0: /** michael@0: * When hasProgress is true, indicates the total number of bytes to be michael@0: * transferred before the download finishes, that can be zero for empty files. michael@0: * michael@0: * When hasProgress is false, this property is always zero. michael@0: */ michael@0: totalBytes: 0, michael@0: michael@0: /** michael@0: * Number of bytes currently transferred. This value starts at zero, and may michael@0: * be updated regardless of the value of hasProgress. michael@0: * michael@0: * @note You shouldn't rely on this property being equal to totalBytes to michael@0: * determine whether the download is completed. You should use the michael@0: * individual state properties instead. michael@0: */ michael@0: currentBytes: 0, michael@0: michael@0: /** michael@0: * Fractional number representing the speed of the download, in bytes per michael@0: * second. This value is zero when the download is stopped, and may be michael@0: * updated regardless of the value of hasProgress. michael@0: */ michael@0: speed: 0, michael@0: michael@0: /** michael@0: * Indicates whether, at this time, there is any partially downloaded data michael@0: * that can be used when restarting a failed or canceled download. michael@0: * michael@0: * This property is relevant while the download is in progress, and also if it michael@0: * failed or has been canceled. If the download has been completed michael@0: * successfully, this property is always false. michael@0: * michael@0: * Whether partial data can actually be retained depends on the saver and the michael@0: * download source, and may not be known before the download is started. michael@0: */ michael@0: hasPartialData: false, michael@0: michael@0: /** michael@0: * This can be set to a function that is called after other properties change. michael@0: */ michael@0: onchange: null, michael@0: michael@0: /** michael@0: * This tells if the user has chosen to open/run the downloaded file after michael@0: * download has completed. michael@0: */ michael@0: launchWhenSucceeded: false, michael@0: michael@0: /** michael@0: * This represents the MIME type of the download. michael@0: */ michael@0: contentType: null, michael@0: michael@0: /** michael@0: * This indicates the path of the application to be used to launch the file, michael@0: * or null if the file should be launched with the default application. michael@0: */ michael@0: launcherPath: null, michael@0: michael@0: /** michael@0: * Raises the onchange notification. michael@0: */ michael@0: _notifyChange: function D_notifyChange() { michael@0: try { michael@0: if (this.onchange) { michael@0: this.onchange(); michael@0: } michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The download may be stopped and restarted multiple times before it michael@0: * completes successfully. This may happen if any of the download attempts is michael@0: * canceled or fails. michael@0: * michael@0: * This property contains a promise that is linked to the current attempt, or michael@0: * null if the download is either stopped or in the process of being canceled. michael@0: * If the download restarts, this property is replaced with a new promise. michael@0: * michael@0: * The promise is resolved if the attempt it represents finishes successfully, michael@0: * and rejected if the attempt fails. michael@0: */ michael@0: _currentAttempt: null, michael@0: michael@0: /** michael@0: * Starts the download for the first time, or restarts a download that failed michael@0: * or has been canceled. michael@0: * michael@0: * Calling this method when the download has been completed successfully has michael@0: * no effect, and the method returns a resolved promise. If the download is michael@0: * in progress, the method returns the same promise as the previous call. michael@0: * michael@0: * If the "cancel" method was called but the cancellation process has not michael@0: * finished yet, this method waits for the cancellation to finish, then michael@0: * restarts the download immediately. michael@0: * michael@0: * @note If you need to start a new download from the same source, rather than michael@0: * restarting a failed or canceled one, you should create a separate michael@0: * Download object with the same source as the current one. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has finished successfully. michael@0: * @rejects JavaScript exception if the download failed. michael@0: */ michael@0: start: function D_start() michael@0: { michael@0: // If the download succeeded, it's the final state, we have nothing to do. michael@0: if (this.succeeded) { michael@0: return Promise.resolve(); michael@0: } michael@0: michael@0: // If the download already started and hasn't failed or hasn't been michael@0: // canceled, return the same promise as the previous call, allowing the michael@0: // caller to wait for the current attempt to finish. michael@0: if (this._currentAttempt) { michael@0: return this._currentAttempt; michael@0: } michael@0: michael@0: // While shutting down or disposing of this object, we prevent the download michael@0: // from returning to be in progress. michael@0: if (this._finalized) { michael@0: return Promise.reject(new DownloadError({ michael@0: message: "Cannot start after finalization."})); michael@0: } michael@0: michael@0: // Initialize all the status properties for a new or restarted download. michael@0: this.stopped = false; michael@0: this.canceled = false; michael@0: this.error = null; michael@0: this.hasProgress = false; michael@0: this.progress = 0; michael@0: this.totalBytes = 0; michael@0: this.currentBytes = 0; michael@0: this.startTime = new Date(); michael@0: michael@0: // Create a new deferred object and an associated promise before starting michael@0: // the actual download. We store it on the download as the current attempt. michael@0: let deferAttempt = Promise.defer(); michael@0: let currentAttempt = deferAttempt.promise; michael@0: this._currentAttempt = currentAttempt; michael@0: michael@0: // Restart the progress and speed calculations from scratch. michael@0: this._lastProgressTimeMs = 0; michael@0: michael@0: // This function propagates progress from the DownloadSaver object, unless michael@0: // it comes in late from a download attempt that was replaced by a new one. michael@0: // If the cancellation process for the download has started, then the update michael@0: // is ignored. michael@0: function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData) michael@0: { michael@0: if (this._currentAttempt == currentAttempt) { michael@0: this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData); michael@0: } michael@0: } michael@0: michael@0: // This function propagates download properties from the DownloadSaver michael@0: // object, unless it comes in late from a download attempt that was michael@0: // replaced by a new one. If the cancellation process for the download has michael@0: // started, then the update is ignored. michael@0: function DS_setProperties(aOptions) michael@0: { michael@0: if (this._currentAttempt != currentAttempt) { michael@0: return; michael@0: } michael@0: michael@0: let changeMade = false; michael@0: michael@0: if ("contentType" in aOptions && michael@0: this.contentType != aOptions.contentType) { michael@0: this.contentType = aOptions.contentType; michael@0: changeMade = true; michael@0: } michael@0: michael@0: if (changeMade) { michael@0: this._notifyChange(); michael@0: } michael@0: } michael@0: michael@0: // Now that we stored the promise in the download object, we can start the michael@0: // task that will actually execute the download. michael@0: deferAttempt.resolve(Task.spawn(function task_D_start() { michael@0: // Wait upon any pending operation before restarting. michael@0: if (this._promiseCanceled) { michael@0: yield this._promiseCanceled; michael@0: } michael@0: if (this._promiseRemovePartialData) { michael@0: try { michael@0: yield this._promiseRemovePartialData; michael@0: } catch (ex) { michael@0: // Ignore any errors, which are already reported by the original michael@0: // caller of the removePartialData method. michael@0: } michael@0: } michael@0: michael@0: // In case the download was restarted while cancellation was in progress, michael@0: // but the previous attempt actually succeeded before cancellation could michael@0: // be processed, it is possible that the download has already finished. michael@0: if (this.succeeded) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: // Disallow download if parental controls service restricts it. michael@0: if (yield DownloadIntegration.shouldBlockForParentalControls(this)) { michael@0: throw new DownloadError({ becauseBlockedByParentalControls: true }); michael@0: } michael@0: michael@0: // We should check if we have been canceled in the meantime, after all michael@0: // the previous asynchronous operations have been executed and just michael@0: // before we call the "execute" method of the saver. michael@0: if (this._promiseCanceled) { michael@0: // The exception will become a cancellation in the "catch" block. michael@0: throw undefined; michael@0: } michael@0: michael@0: // Execute the actual download through the saver object. michael@0: this._saverExecuting = true; michael@0: yield this.saver.execute(DS_setProgressBytes.bind(this), michael@0: DS_setProperties.bind(this)); michael@0: michael@0: // Check for application reputation, which requires the entire file to michael@0: // be downloaded. After that, check for the last time if the download michael@0: // has been canceled. Both cases require the target file to be deleted, michael@0: // thus we process both in the same block of code. michael@0: if ((yield DownloadIntegration.shouldBlockForReputationCheck(this)) || michael@0: this._promiseCanceled) { michael@0: try { michael@0: yield OS.File.remove(this.target.path); michael@0: } catch (ex) { michael@0: Cu.reportError(ex); michael@0: } michael@0: // If this is actually a cancellation, this exception will be changed michael@0: // in the catch block below. michael@0: throw new DownloadError({ becauseBlockedByReputationCheck: true }); michael@0: } michael@0: michael@0: // Update the status properties for a successful download. michael@0: this.progress = 100; michael@0: this.succeeded = true; michael@0: this.hasPartialData = false; michael@0: } catch (ex) { michael@0: // Fail with a generic status code on cancellation, so that the caller michael@0: // is forced to actually check the status properties to see if the michael@0: // download was canceled or failed because of other reasons. michael@0: if (this._promiseCanceled) { michael@0: throw new DownloadError({ message: "Download canceled." }); michael@0: } michael@0: michael@0: // An HTTP 450 error code is used by Windows to indicate that a uri is michael@0: // blocked by parental controls. This will prevent the download from michael@0: // occuring, so an error needs to be raised. This is not performed michael@0: // during the parental controls check above as it requires the request michael@0: // to start. michael@0: if (this._blockedByParentalControls) { michael@0: ex = new DownloadError({ becauseBlockedByParentalControls: true }); michael@0: } michael@0: michael@0: // Update the download error, unless a new attempt already started. The michael@0: // change in the status property is notified in the finally block. michael@0: if (this._currentAttempt == currentAttempt || !this._currentAttempt) { michael@0: this.error = ex; michael@0: } michael@0: throw ex; michael@0: } finally { michael@0: // Any cancellation request has now been processed. michael@0: this._saverExecuting = false; michael@0: this._promiseCanceled = null; michael@0: michael@0: // Update the status properties, unless a new attempt already started. michael@0: if (this._currentAttempt == currentAttempt || !this._currentAttempt) { michael@0: this._currentAttempt = null; michael@0: this.stopped = true; michael@0: this.speed = 0; michael@0: this._notifyChange(); michael@0: if (this.succeeded) { michael@0: yield DownloadIntegration.downloadDone(this); michael@0: michael@0: this._deferSucceeded.resolve(); michael@0: michael@0: if (this.launchWhenSucceeded) { michael@0: this.launch().then(null, Cu.reportError); michael@0: michael@0: // Always schedule files to be deleted at the end of the private browsing michael@0: // mode, regardless of the value of the pref. michael@0: if (this.source.isPrivate) { michael@0: gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible( michael@0: new FileUtils.File(this.target.path)); michael@0: } else if (Services.prefs.getBoolPref( michael@0: "browser.helperApps.deleteTempFileOnExit")) { michael@0: gExternalAppLauncher.deleteTemporaryFileOnExit( michael@0: new FileUtils.File(this.target.path)); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }.bind(this))); michael@0: michael@0: // Notify the new download state before returning. michael@0: this._notifyChange(); michael@0: return currentAttempt; michael@0: }, michael@0: michael@0: /* michael@0: * Launches the file after download has completed. This can open michael@0: * the file with the default application for the target MIME type michael@0: * or file extension, or with a custom application if launcherPath michael@0: * is set. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the instruction to launch the file has been michael@0: * successfully given to the operating system. Note that michael@0: * the OS might still take a while until the file is actually michael@0: * launched. michael@0: * @rejects JavaScript exception if there was an error trying to launch michael@0: * the file. michael@0: */ michael@0: launch: function() { michael@0: if (!this.succeeded) { michael@0: return Promise.reject( michael@0: new Error("launch can only be called if the download succeeded") michael@0: ); michael@0: } michael@0: michael@0: return DownloadIntegration.launchDownload(this); michael@0: }, michael@0: michael@0: /* michael@0: * Shows the folder containing the target file, or where the target file michael@0: * will be saved. This may be called at any time, even if the download michael@0: * failed or is currently in progress. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the instruction to open the containing folder has been michael@0: * successfully given to the operating system. Note that michael@0: * the OS might still take a while until the folder is actually michael@0: * opened. michael@0: * @rejects JavaScript exception if there was an error trying to open michael@0: * the containing folder. michael@0: */ michael@0: showContainingDirectory: function D_showContainingDirectory() { michael@0: return DownloadIntegration.showContainingDirectory(this.target.path); michael@0: }, michael@0: michael@0: /** michael@0: * When a request to cancel the download is received, contains a promise that michael@0: * will be resolved when the cancellation request is processed. When the michael@0: * request is processed, this property becomes null again. michael@0: */ michael@0: _promiseCanceled: null, michael@0: michael@0: /** michael@0: * True between the call to the "execute" method of the saver and the michael@0: * completion of the current download attempt. michael@0: */ michael@0: _saverExecuting: false, michael@0: michael@0: /** michael@0: * Cancels the download. michael@0: * michael@0: * The cancellation request is asynchronous. Until the cancellation process michael@0: * finishes, temporary files or part files may still exist even if they are michael@0: * expected to be deleted. michael@0: * michael@0: * In case the download completes successfully before the cancellation request michael@0: * could be processed, this method has no effect, and it returns a resolved michael@0: * promise. You should check the properties of the download at the time the michael@0: * returned promise is resolved to determine if the download was cancelled. michael@0: * michael@0: * Calling this method when the download has been completed successfully, michael@0: * failed, or has been canceled has no effect, and the method returns a michael@0: * resolved promise. This behavior is designed for the case where the call michael@0: * to "cancel" happens asynchronously, and is consistent with the case where michael@0: * the cancellation request could not be processed in time. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the cancellation process has finished. michael@0: * @rejects Never. michael@0: */ michael@0: cancel: function D_cancel() michael@0: { michael@0: // If the download is currently stopped, we have nothing to do. michael@0: if (this.stopped) { michael@0: return Promise.resolve(); michael@0: } michael@0: michael@0: if (!this._promiseCanceled) { michael@0: // Start a new cancellation request. michael@0: let deferCanceled = Promise.defer(); michael@0: this._currentAttempt.then(function () deferCanceled.resolve(), michael@0: function () deferCanceled.resolve()); michael@0: this._promiseCanceled = deferCanceled.promise; michael@0: michael@0: // The download can already be restarted. michael@0: this._currentAttempt = null; michael@0: michael@0: // Notify that the cancellation request was received. michael@0: this.canceled = true; michael@0: this._notifyChange(); michael@0: michael@0: // Execute the actual cancellation through the saver object, in case it michael@0: // has already started. Otherwise, the cancellation will be handled just michael@0: // before the saver is started. michael@0: if (this._saverExecuting) { michael@0: this.saver.cancel(); michael@0: } michael@0: } michael@0: michael@0: return this._promiseCanceled; michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether any partially downloaded data should be retained, to use michael@0: * when restarting a failed or canceled download. The default is false. michael@0: * michael@0: * Whether partial data can actually be retained depends on the saver and the michael@0: * download source, and may not be known before the download is started. michael@0: * michael@0: * To have any effect, this property must be set before starting the download. michael@0: * Resetting this property to false after the download has already started michael@0: * will not remove any partial data. michael@0: * michael@0: * If this property is set to true, care should be taken that partial data is michael@0: * removed before the reference to the download is discarded. This can be michael@0: * done using the removePartialData or the "finalize" methods. michael@0: */ michael@0: tryToKeepPartialData: false, michael@0: michael@0: /** michael@0: * When a request to remove partially downloaded data is received, contains a michael@0: * promise that will be resolved when the removal request is processed. When michael@0: * the request is processed, this property becomes null again. michael@0: */ michael@0: _promiseRemovePartialData: null, michael@0: michael@0: /** michael@0: * Removes any partial data kept as part of a canceled or failed download. michael@0: * michael@0: * If the download is not canceled or failed, this method has no effect, and michael@0: * it returns a resolved promise. If the "cancel" method was called but the michael@0: * cancellation process has not finished yet, this method waits for the michael@0: * cancellation to finish, then removes the partial data. michael@0: * michael@0: * After this method has been called, if the tryToKeepPartialData property is michael@0: * still true when the download is restarted, partial data will be retained michael@0: * during the new download attempt. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the partial data has been successfully removed. michael@0: * @rejects JavaScript exception if the operation could not be completed. michael@0: */ michael@0: removePartialData: function () michael@0: { michael@0: if (!this.canceled && !this.error) { michael@0: return Promise.resolve(); michael@0: } michael@0: michael@0: let promiseRemovePartialData = this._promiseRemovePartialData; michael@0: michael@0: if (!promiseRemovePartialData) { michael@0: let deferRemovePartialData = Promise.defer(); michael@0: promiseRemovePartialData = deferRemovePartialData.promise; michael@0: this._promiseRemovePartialData = promiseRemovePartialData; michael@0: michael@0: deferRemovePartialData.resolve( michael@0: Task.spawn(function task_D_removePartialData() { michael@0: try { michael@0: // Wait upon any pending cancellation request. michael@0: if (this._promiseCanceled) { michael@0: yield this._promiseCanceled; michael@0: } michael@0: // Ask the saver object to remove any partial data. michael@0: yield this.saver.removePartialData(); michael@0: // For completeness, clear the number of bytes transferred. michael@0: if (this.currentBytes != 0 || this.hasPartialData) { michael@0: this.currentBytes = 0; michael@0: this.hasPartialData = false; michael@0: this._notifyChange(); michael@0: } michael@0: } finally { michael@0: this._promiseRemovePartialData = null; michael@0: } michael@0: }.bind(this))); michael@0: } michael@0: michael@0: return promiseRemovePartialData; michael@0: }, michael@0: michael@0: /** michael@0: * This deferred object contains a promise that is resolved as soon as this michael@0: * download finishes successfully, and is never rejected. This property is michael@0: * initialized when the download is created, and never changes. michael@0: */ michael@0: _deferSucceeded: null, michael@0: michael@0: /** michael@0: * Returns a promise that is resolved as soon as this download finishes michael@0: * successfully, even if the download was stopped and restarted meanwhile. michael@0: * michael@0: * You can use this property for scheduling download completion actions in the michael@0: * current session, for downloads that are controlled interactively. If the michael@0: * download is not controlled interactively, you should use the promise michael@0: * returned by the "start" method instead, to check for success or failure. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has finished successfully. michael@0: * @rejects Never. michael@0: */ michael@0: whenSucceeded: function D_whenSucceeded() michael@0: { michael@0: return this._deferSucceeded.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Updates the state of a finished, failed, or canceled download based on the michael@0: * current state in the file system. If the download is in progress or it has michael@0: * been finalized, this method has no effect, and it returns a resolved michael@0: * promise. michael@0: * michael@0: * This allows the properties of the download to be updated in case the user michael@0: * moved or deleted the target file or its associated ".part" file. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the operation has completed. michael@0: * @rejects Never. michael@0: */ michael@0: refresh: function () michael@0: { michael@0: return Task.spawn(function () { michael@0: if (!this.stopped || this._finalized) { michael@0: return; michael@0: } michael@0: michael@0: // Update the current progress from disk if we retained partial data. michael@0: if (this.hasPartialData && this.target.partFilePath) { michael@0: let stat = yield OS.File.stat(this.target.partFilePath); michael@0: michael@0: // Ignore the result if the state has changed meanwhile. michael@0: if (!this.stopped || this._finalized) { michael@0: return; michael@0: } michael@0: michael@0: // Update the bytes transferred and the related progress properties. michael@0: this.currentBytes = stat.size; michael@0: if (this.totalBytes > 0) { michael@0: this.hasProgress = true; michael@0: this.progress = Math.floor(this.currentBytes / michael@0: this.totalBytes * 100); michael@0: } michael@0: this._notifyChange(); michael@0: } michael@0: }.bind(this)).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: /** michael@0: * True if the "finalize" method has been called. This prevents the download michael@0: * from starting again after having been stopped. michael@0: */ michael@0: _finalized: false, michael@0: michael@0: /** michael@0: * Ensures that the download is stopped, and optionally removes any partial michael@0: * data kept as part of a canceled or failed download. After this method has michael@0: * been called, the download cannot be started again. michael@0: * michael@0: * This method should be used in place of "cancel" and removePartialData while michael@0: * shutting down or disposing of the download object, to prevent other callers michael@0: * from interfering with the operation. This is required because cancellation michael@0: * and other operations are asynchronous. michael@0: * michael@0: * @param aRemovePartialData michael@0: * Whether any partially downloaded data should be removed after the michael@0: * download has been stopped. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the operation has finished successfully. michael@0: * @rejects JavaScript exception if an error occurred while removing the michael@0: * partially downloaded data. michael@0: */ michael@0: finalize: function (aRemovePartialData) michael@0: { michael@0: // Prevents the download from starting again after having been stopped. michael@0: this._finalized = true; michael@0: michael@0: if (aRemovePartialData) { michael@0: // Cancel the download, in case it is currently in progress, then remove michael@0: // any partially downloaded data. The removal operation waits for michael@0: // cancellation to be completed before resolving the promise it returns. michael@0: this.cancel(); michael@0: return this.removePartialData(); michael@0: } else { michael@0: // Just cancel the download, in case it is currently in progress. michael@0: return this.cancel(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Indicates the time of the last progress notification, expressed as the michael@0: * number of milliseconds since January 1, 1970, 00:00:00 UTC. This is zero michael@0: * until some bytes have actually been transferred. michael@0: */ michael@0: _lastProgressTimeMs: 0, michael@0: michael@0: /** michael@0: * Updates progress notifications based on the number of bytes transferred. michael@0: * michael@0: * The number of bytes transferred is not updated unless enough time passed michael@0: * since this function was last called. This limits the computation load, in michael@0: * particular when the listeners update the user interface in response. michael@0: * michael@0: * @param aCurrentBytes michael@0: * Number of bytes transferred until now. michael@0: * @param aTotalBytes michael@0: * Total number of bytes to be transferred, or -1 if unknown. michael@0: * @param aHasPartialData michael@0: * Indicates whether the partially downloaded data can be used when michael@0: * restarting the download if it fails or is canceled. michael@0: */ michael@0: _setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) { michael@0: let changeMade = (this.hasPartialData != aHasPartialData); michael@0: this.hasPartialData = aHasPartialData; michael@0: michael@0: // Unless aTotalBytes is -1, we can report partial download progress. In michael@0: // this case, notify when the related properties changed since last time. michael@0: if (aTotalBytes != -1 && (!this.hasProgress || michael@0: this.totalBytes != aTotalBytes)) { michael@0: this.hasProgress = true; michael@0: this.totalBytes = aTotalBytes; michael@0: changeMade = true; michael@0: } michael@0: michael@0: // Updating the progress and computing the speed require that enough time michael@0: // passed since the last update, or that we haven't started throttling yet. michael@0: let currentTimeMs = Date.now(); michael@0: let intervalMs = currentTimeMs - this._lastProgressTimeMs; michael@0: if (intervalMs >= kProgressUpdateIntervalMs) { michael@0: // Don't compute the speed unless we started throttling notifications. michael@0: if (this._lastProgressTimeMs != 0) { michael@0: // Calculate the speed in bytes per second. michael@0: let rawSpeed = (aCurrentBytes - this.currentBytes) / intervalMs * 1000; michael@0: if (this.speed == 0) { michael@0: // When the previous speed is exactly zero instead of a fractional michael@0: // number, this can be considered the first element of the series. michael@0: this.speed = rawSpeed; michael@0: } else { michael@0: // Apply exponential smoothing, with a smoothing factor of 0.1. michael@0: this.speed = rawSpeed * 0.1 + this.speed * 0.9; michael@0: } michael@0: } michael@0: michael@0: // Start throttling notifications only when we have actually received some michael@0: // bytes for the first time. The timing of the first part of the download michael@0: // is not reliable, due to possible latency in the initial notifications. michael@0: // This also allows automated tests to receive and verify the number of michael@0: // bytes initially transferred. michael@0: if (aCurrentBytes > 0) { michael@0: this._lastProgressTimeMs = currentTimeMs; michael@0: michael@0: // Update the progress now that we don't need its previous value. michael@0: this.currentBytes = aCurrentBytes; michael@0: if (this.totalBytes > 0) { michael@0: this.progress = Math.floor(this.currentBytes / this.totalBytes * 100); michael@0: } michael@0: changeMade = true; michael@0: } michael@0: } michael@0: michael@0: if (changeMade) { michael@0: this._notifyChange(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns a static representation of the current object state. michael@0: * michael@0: * @return A JavaScript object that can be serialized to JSON. michael@0: */ michael@0: toSerializable: function () michael@0: { michael@0: let serializable = { michael@0: source: this.source.toSerializable(), michael@0: target: this.target.toSerializable(), michael@0: }; michael@0: michael@0: // Simplify the representation for the most common saver type. If the saver michael@0: // is an object instead of a simple string, we can't simplify it because we michael@0: // need to persist all its properties, not only "type". This may happen for michael@0: // savers of type "copy" as well as other types. michael@0: let saver = this.saver.toSerializable(); michael@0: if (saver !== "copy") { michael@0: serializable.saver = saver; michael@0: } michael@0: michael@0: if (this.error && ("message" in this.error)) { michael@0: serializable.error = { message: this.error.message }; michael@0: } michael@0: michael@0: if (this.startTime) { michael@0: serializable.startTime = this.startTime.toJSON(); michael@0: } michael@0: michael@0: // These are serialized unless they are false, null, or empty strings. michael@0: for (let property of kSerializableDownloadProperties) { michael@0: if (property != "error" && property != "startTime" && this[property]) { michael@0: serializable[property] = this[property]; michael@0: } michael@0: } michael@0: michael@0: serializeUnknownProperties(this, serializable); michael@0: michael@0: return serializable; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a value that changes only when one of the properties of a Download michael@0: * object that should be saved into a file also change. This excludes michael@0: * properties whose value doesn't usually change during the download lifetime. michael@0: * michael@0: * This function is used to determine whether the download should be michael@0: * serialized after a property change notification has been received. michael@0: * michael@0: * @return String representing the relevant download state. michael@0: */ michael@0: getSerializationHash: function () michael@0: { michael@0: // The "succeeded", "canceled", "error", and startTime properties are not michael@0: // taken into account because they all change before the "stopped" property michael@0: // changes, and are not altered in other cases. michael@0: return this.stopped + "," + this.totalBytes + "," + this.hasPartialData + michael@0: "," + this.contentType; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Defines which properties of the Download object are serializable. michael@0: */ michael@0: const kSerializableDownloadProperties = [ michael@0: "succeeded", michael@0: "canceled", michael@0: "error", michael@0: "totalBytes", michael@0: "hasPartialData", michael@0: "tryToKeepPartialData", michael@0: "launcherPath", michael@0: "launchWhenSucceeded", michael@0: "contentType", michael@0: ]; michael@0: michael@0: /** michael@0: * Creates a new Download object from a serializable representation. This michael@0: * function is used by the createDownload method of Downloads.jsm when a new michael@0: * Download object is requested, thus some properties may refer to live objects michael@0: * in place of their serializable representations. michael@0: * michael@0: * @param aSerializable michael@0: * An object with the following fields: michael@0: * { michael@0: * source: DownloadSource object, or its serializable representation. michael@0: * See DownloadSource.fromSerializable for details. michael@0: * target: DownloadTarget object, or its serializable representation. michael@0: * See DownloadTarget.fromSerializable for details. michael@0: * saver: Serializable representation of a DownloadSaver object. See michael@0: * DownloadSaver.fromSerializable for details. If omitted, michael@0: * defaults to "copy". michael@0: * } michael@0: * michael@0: * @return The newly created Download object. michael@0: */ michael@0: Download.fromSerializable = function (aSerializable) { michael@0: let download = new Download(); michael@0: if (aSerializable.source instanceof DownloadSource) { michael@0: download.source = aSerializable.source; michael@0: } else { michael@0: download.source = DownloadSource.fromSerializable(aSerializable.source); michael@0: } michael@0: if (aSerializable.target instanceof DownloadTarget) { michael@0: download.target = aSerializable.target; michael@0: } else { michael@0: download.target = DownloadTarget.fromSerializable(aSerializable.target); michael@0: } michael@0: if ("saver" in aSerializable) { michael@0: download.saver = DownloadSaver.fromSerializable(aSerializable.saver); michael@0: } else { michael@0: download.saver = DownloadSaver.fromSerializable("copy"); michael@0: } michael@0: download.saver.download = download; michael@0: michael@0: if ("startTime" in aSerializable) { michael@0: let time = aSerializable.startTime.getTime michael@0: ? aSerializable.startTime.getTime() michael@0: : aSerializable.startTime; michael@0: download.startTime = new Date(time); michael@0: } michael@0: michael@0: for (let property of kSerializableDownloadProperties) { michael@0: if (property in aSerializable) { michael@0: download[property] = aSerializable[property]; michael@0: } michael@0: } michael@0: michael@0: deserializeUnknownProperties(download, aSerializable, property => michael@0: kSerializableDownloadProperties.indexOf(property) == -1 && michael@0: property != "startTime" && michael@0: property != "source" && michael@0: property != "target" && michael@0: property != "saver"); michael@0: michael@0: return download; michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadSource michael@0: michael@0: /** michael@0: * Represents the source of a download, for example a document or an URI. michael@0: */ michael@0: this.DownloadSource = function () {} michael@0: michael@0: this.DownloadSource.prototype = { michael@0: /** michael@0: * String containing the URI for the download source. michael@0: */ michael@0: url: null, michael@0: michael@0: /** michael@0: * Indicates whether the download originated from a private window. This michael@0: * determines the context of the network request that is made to retrieve the michael@0: * resource. michael@0: */ michael@0: isPrivate: false, michael@0: michael@0: /** michael@0: * String containing the referrer URI of the download source, or null if no michael@0: * referrer should be sent or the download source is not HTTP. michael@0: */ michael@0: referrer: null, michael@0: michael@0: /** michael@0: * Returns a static representation of the current object state. michael@0: * michael@0: * @return A JavaScript object that can be serialized to JSON. michael@0: */ michael@0: toSerializable: function () michael@0: { michael@0: // Simplify the representation if we don't have other details. michael@0: if (!this.isPrivate && !this.referrer && !this._unknownProperties) { michael@0: return this.url; michael@0: } michael@0: michael@0: let serializable = { url: this.url }; michael@0: if (this.isPrivate) { michael@0: serializable.isPrivate = true; michael@0: } michael@0: if (this.referrer) { michael@0: serializable.referrer = this.referrer; michael@0: } michael@0: michael@0: serializeUnknownProperties(this, serializable); michael@0: return serializable; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Creates a new DownloadSource object from its serializable representation. michael@0: * michael@0: * @param aSerializable michael@0: * Serializable representation of a DownloadSource object. This may be a michael@0: * string containing the URI for the download source, an nsIURI, or an michael@0: * object with the following properties: michael@0: * { michael@0: * url: String containing the URI for the download source. michael@0: * isPrivate: Indicates whether the download originated from a private michael@0: * window. If omitted, the download is public. michael@0: * referrer: String containing the referrer URI of the download source. michael@0: * Can be omitted or null if no referrer should be sent or michael@0: * the download source is not HTTP. michael@0: * } michael@0: * michael@0: * @return The newly created DownloadSource object. michael@0: */ michael@0: this.DownloadSource.fromSerializable = function (aSerializable) { michael@0: let source = new DownloadSource(); michael@0: if (isString(aSerializable)) { michael@0: // Convert String objects to primitive strings at this point. michael@0: source.url = aSerializable.toString(); michael@0: } else if (aSerializable instanceof Ci.nsIURI) { michael@0: source.url = aSerializable.spec; michael@0: } else { michael@0: // Convert String objects to primitive strings at this point. michael@0: source.url = aSerializable.url.toString(); michael@0: if ("isPrivate" in aSerializable) { michael@0: source.isPrivate = aSerializable.isPrivate; michael@0: } michael@0: if ("referrer" in aSerializable) { michael@0: source.referrer = aSerializable.referrer; michael@0: } michael@0: michael@0: deserializeUnknownProperties(source, aSerializable, property => michael@0: property != "url" && property != "isPrivate" && property != "referrer"); michael@0: } michael@0: michael@0: return source; michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadTarget michael@0: michael@0: /** michael@0: * Represents the target of a download, for example a file in the global michael@0: * downloads directory, or a file in the system temporary directory. michael@0: */ michael@0: this.DownloadTarget = function () {} michael@0: michael@0: this.DownloadTarget.prototype = { michael@0: /** michael@0: * String containing the path of the target file. michael@0: */ michael@0: path: null, michael@0: michael@0: /** michael@0: * String containing the path of the ".part" file containing the data michael@0: * downloaded so far, or null to disable the use of a ".part" file to keep michael@0: * partially downloaded data. michael@0: */ michael@0: partFilePath: null, michael@0: michael@0: /** michael@0: * Returns a static representation of the current object state. michael@0: * michael@0: * @return A JavaScript object that can be serialized to JSON. michael@0: */ michael@0: toSerializable: function () michael@0: { michael@0: // Simplify the representation if we don't have other details. michael@0: if (!this.partFilePath && !this._unknownProperties) { michael@0: return this.path; michael@0: } michael@0: michael@0: let serializable = { path: this.path, michael@0: partFilePath: this.partFilePath }; michael@0: serializeUnknownProperties(this, serializable); michael@0: return serializable; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Creates a new DownloadTarget object from its serializable representation. michael@0: * michael@0: * @param aSerializable michael@0: * Serializable representation of a DownloadTarget object. This may be a michael@0: * string containing the path of the target file, an nsIFile, or an michael@0: * object with the following properties: michael@0: * { michael@0: * path: String containing the path of the target file. michael@0: * partFilePath: optional string containing the part file path. michael@0: * } michael@0: * michael@0: * @return The newly created DownloadTarget object. michael@0: */ michael@0: this.DownloadTarget.fromSerializable = function (aSerializable) { michael@0: let target = new DownloadTarget(); michael@0: if (isString(aSerializable)) { michael@0: // Convert String objects to primitive strings at this point. michael@0: target.path = aSerializable.toString(); michael@0: } else if (aSerializable instanceof Ci.nsIFile) { michael@0: // Read the "path" property of nsIFile after checking the object type. michael@0: target.path = aSerializable.path; michael@0: } else { michael@0: // Read the "path" property of the serializable DownloadTarget michael@0: // representation, converting String objects to primitive strings. michael@0: target.path = aSerializable.path.toString(); michael@0: if ("partFilePath" in aSerializable) { michael@0: target.partFilePath = aSerializable.partFilePath; michael@0: } michael@0: michael@0: deserializeUnknownProperties(target, aSerializable, property => michael@0: property != "path" && property != "partFilePath"); michael@0: } michael@0: return target; michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadError michael@0: michael@0: /** michael@0: * Provides detailed information about a download failure. michael@0: * michael@0: * @param aProperties michael@0: * Object which may contain any of the following properties: michael@0: * { michael@0: * result: Result error code, defaulting to Cr.NS_ERROR_FAILURE michael@0: * message: String error message to be displayed, or null to use the michael@0: * message associated with the result code. michael@0: * inferCause: If true, attempts to determine if the cause of the michael@0: * download is a network failure or a local file failure, michael@0: * based on a set of known values of the result code. michael@0: * This is useful when the error is received by a michael@0: * component that handles both aspects of the download. michael@0: * } michael@0: * The properties object may also contain any of the DownloadError's michael@0: * because properties, which will be set accordingly in the error object. michael@0: */ michael@0: this.DownloadError = function (aProperties) michael@0: { michael@0: const NS_ERROR_MODULE_BASE_OFFSET = 0x45; michael@0: const NS_ERROR_MODULE_NETWORK = 6; michael@0: const NS_ERROR_MODULE_FILES = 13; michael@0: michael@0: // Set the error name used by the Error object prototype first. michael@0: this.name = "DownloadError"; michael@0: this.result = aProperties.result || Cr.NS_ERROR_FAILURE; michael@0: if (aProperties.message) { michael@0: this.message = aProperties.message; michael@0: } else if (aProperties.becauseBlocked || michael@0: aProperties.becauseBlockedByParentalControls || michael@0: aProperties.becauseBlockedByReputationCheck) { michael@0: this.message = "Download blocked."; michael@0: } else { michael@0: let exception = new Components.Exception("", this.result); michael@0: this.message = exception.toString(); michael@0: } michael@0: if (aProperties.inferCause) { michael@0: let module = ((this.result & 0x7FFF0000) >> 16) - michael@0: NS_ERROR_MODULE_BASE_OFFSET; michael@0: this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK); michael@0: this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES); michael@0: } michael@0: else { michael@0: if (aProperties.becauseSourceFailed) { michael@0: this.becauseSourceFailed = true; michael@0: } michael@0: if (aProperties.becauseTargetFailed) { michael@0: this.becauseTargetFailed = true; michael@0: } michael@0: } michael@0: michael@0: if (aProperties.becauseBlockedByParentalControls) { michael@0: this.becauseBlocked = true; michael@0: this.becauseBlockedByParentalControls = true; michael@0: } else if (aProperties.becauseBlockedByReputationCheck) { michael@0: this.becauseBlocked = true; michael@0: this.becauseBlockedByReputationCheck = true; michael@0: } else if (aProperties.becauseBlocked) { michael@0: this.becauseBlocked = true; michael@0: } michael@0: michael@0: this.stack = new Error().stack; michael@0: } michael@0: michael@0: this.DownloadError.prototype = { michael@0: __proto__: Error.prototype, michael@0: michael@0: /** michael@0: * The result code associated with this error. michael@0: */ michael@0: result: false, michael@0: michael@0: /** michael@0: * Indicates an error occurred while reading from the remote location. michael@0: */ michael@0: becauseSourceFailed: false, michael@0: michael@0: /** michael@0: * Indicates an error occurred while writing to the local target. michael@0: */ michael@0: becauseTargetFailed: false, michael@0: michael@0: /** michael@0: * Indicates the download failed because it was blocked. If the reason for michael@0: * blocking is known, the corresponding property will be also set. michael@0: */ michael@0: becauseBlocked: false, michael@0: michael@0: /** michael@0: * Indicates the download was blocked because downloads are globally michael@0: * disallowed by the Parental Controls or Family Safety features on Windows. michael@0: */ michael@0: becauseBlockedByParentalControls: false, michael@0: michael@0: /** michael@0: * Indicates the download was blocked because it failed the reputation check michael@0: * and may be malware. michael@0: */ michael@0: becauseBlockedByReputationCheck: false, michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadSaver michael@0: michael@0: /** michael@0: * Template for an object that actually transfers the data for the download. michael@0: */ michael@0: this.DownloadSaver = function () {} michael@0: michael@0: this.DownloadSaver.prototype = { michael@0: /** michael@0: * Download object for raising notifications and reading properties. michael@0: * michael@0: * If the tryToKeepPartialData property of the download object is false, the michael@0: * saver should never try to keep partially downloaded data if the download michael@0: * fails. michael@0: */ michael@0: download: null, michael@0: michael@0: /** michael@0: * Executes the download. michael@0: * michael@0: * @param aSetProgressBytesFn michael@0: * This function may be called by the saver to report progress. It michael@0: * takes three arguments: the first is the number of bytes transferred michael@0: * until now, the second is the total number of bytes to be michael@0: * transferred (or -1 if unknown), the third indicates whether the michael@0: * partially downloaded data can be used when restarting the download michael@0: * if it fails or is canceled. michael@0: * @param aSetPropertiesFn michael@0: * This function may be called by the saver to report information michael@0: * about new download properties discovered by the saver during the michael@0: * download process. It takes an object where the keys represents michael@0: * the names of the properties to set, and the value represents the michael@0: * value to set. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the download has finished successfully. michael@0: * @rejects JavaScript exception if the download failed. michael@0: */ michael@0: execute: function DS_execute(aSetProgressBytesFn, aSetPropertiesFn) michael@0: { michael@0: throw new Error("Not implemented."); michael@0: }, michael@0: michael@0: /** michael@0: * Cancels the download. michael@0: */ michael@0: cancel: function DS_cancel() michael@0: { michael@0: throw new Error("Not implemented."); michael@0: }, michael@0: michael@0: /** michael@0: * Removes any partial data kept as part of a canceled or failed download. michael@0: * michael@0: * This method is never called until the promise returned by "execute" is michael@0: * either resolved or rejected, and the "execute" method is not called again michael@0: * until the promise returned by this method is resolved or rejected. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the operation has finished successfully. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: removePartialData: function DS_removePartialData() michael@0: { michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * This can be called by the saver implementation when the download is already michael@0: * started, to add it to the browsing history. This method has no effect if michael@0: * the download is private. michael@0: */ michael@0: addToHistory: function () michael@0: { michael@0: if (this.download.source.isPrivate) { michael@0: return; michael@0: } michael@0: michael@0: let sourceUri = NetUtil.newURI(this.download.source.url); michael@0: let referrer = this.download.source.referrer; michael@0: let referrerUri = referrer ? NetUtil.newURI(referrer) : null; michael@0: let targetUri = NetUtil.newURI(new FileUtils.File( michael@0: this.download.target.path)); michael@0: michael@0: // The start time is always available when we reach this point. michael@0: let startPRTime = this.download.startTime.getTime() * 1000; michael@0: michael@0: gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime, michael@0: targetUri); michael@0: }, michael@0: michael@0: /** michael@0: * Returns a static representation of the current object state. michael@0: * michael@0: * @return A JavaScript object that can be serialized to JSON. michael@0: */ michael@0: toSerializable: function () michael@0: { michael@0: throw new Error("Not implemented."); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the SHA-256 hash of the downloaded file, if it exists. michael@0: */ michael@0: getSha256Hash: function () michael@0: { michael@0: throw new Error("Not implemented."); michael@0: }, michael@0: michael@0: getSignatureInfo: function () michael@0: { michael@0: throw new Error("Not implemented."); michael@0: }, michael@0: }; // DownloadSaver michael@0: michael@0: /** michael@0: * Creates a new DownloadSaver object from its serializable representation. michael@0: * michael@0: * @param aSerializable michael@0: * Serializable representation of a DownloadSaver object. If no initial michael@0: * state information for the saver object is needed, can be a string michael@0: * representing the class of the download operation, for example "copy". michael@0: * michael@0: * @return The newly created DownloadSaver object. michael@0: */ michael@0: this.DownloadSaver.fromSerializable = function (aSerializable) { michael@0: let serializable = isString(aSerializable) ? { type: aSerializable } michael@0: : aSerializable; michael@0: let saver; michael@0: switch (serializable.type) { michael@0: case "copy": michael@0: saver = DownloadCopySaver.fromSerializable(serializable); michael@0: break; michael@0: case "legacy": michael@0: saver = DownloadLegacySaver.fromSerializable(serializable); michael@0: break; michael@0: default: michael@0: throw new Error("Unrecoginzed download saver type."); michael@0: } michael@0: return saver; michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadCopySaver michael@0: michael@0: /** michael@0: * Saver object that simply copies the entire source file to the target. michael@0: */ michael@0: this.DownloadCopySaver = function () {} michael@0: michael@0: this.DownloadCopySaver.prototype = { michael@0: __proto__: DownloadSaver.prototype, michael@0: michael@0: /** michael@0: * BackgroundFileSaver object currently handling the download. michael@0: */ michael@0: _backgroundFileSaver: null, michael@0: michael@0: /** michael@0: * Indicates whether the "cancel" method has been called. This is used to michael@0: * prevent the request from starting in case the operation is canceled before michael@0: * the BackgroundFileSaver instance has been created. michael@0: */ michael@0: _canceled: false, michael@0: michael@0: /** michael@0: * Save the SHA-256 hash in raw bytes of the downloaded file. This is null michael@0: * unless BackgroundFileSaver has successfully completed saving the file. michael@0: */ michael@0: _sha256Hash: null, michael@0: michael@0: /** michael@0: * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert michael@0: * if the file is signed. This is empty if the file is unsigned, and null michael@0: * unless BackgroundFileSaver has successfully completed saving the file. michael@0: */ michael@0: _signatureInfo: null, michael@0: michael@0: /** michael@0: * True if the associated download has already been added to browsing history. michael@0: */ michael@0: alreadyAddedToHistory: false, michael@0: michael@0: /** michael@0: * String corresponding to the entityID property of the nsIResumableChannel michael@0: * used to execute the download, or null if the channel was not resumable or michael@0: * the saver was instructed not to keep partially downloaded data. michael@0: */ michael@0: entityID: null, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.execute". michael@0: */ michael@0: execute: function DCS_execute(aSetProgressBytesFn, aSetPropertiesFn) michael@0: { michael@0: let copySaver = this; michael@0: michael@0: this._canceled = false; michael@0: michael@0: let download = this.download; michael@0: let targetPath = download.target.path; michael@0: let partFilePath = download.target.partFilePath; michael@0: let keepPartialData = download.tryToKeepPartialData; michael@0: michael@0: return Task.spawn(function task_DCS_execute() { michael@0: // Add the download to history the first time it is started in this michael@0: // session. If the download is restarted in a different session, a new michael@0: // history visit will be added. We do this just to avoid the complexity michael@0: // of serializing this state between sessions, since adding a new visit michael@0: // does not have any noticeable side effect. michael@0: if (!this.alreadyAddedToHistory) { michael@0: this.addToHistory(); michael@0: this.alreadyAddedToHistory = true; michael@0: } michael@0: michael@0: // To reduce the chance that other downloads reuse the same final target michael@0: // file name, we should create a placeholder as soon as possible, before michael@0: // starting the network request. The placeholder is also required in case michael@0: // we are using a ".part" file instead of the final target while the michael@0: // download is in progress. michael@0: try { michael@0: // If the file already exists, don't delete its contents yet. michael@0: let file = yield OS.File.open(targetPath, { write: true }); michael@0: yield file.close(); michael@0: } catch (ex if ex instanceof OS.File.Error) { michael@0: // Throw a DownloadError indicating that the operation failed because of michael@0: // the target file. We cannot translate this into a specific result michael@0: // code, but we preserve the original message using the toString method. michael@0: let error = new DownloadError({ message: ex.toString() }); michael@0: error.becauseTargetFailed = true; michael@0: throw error; michael@0: } michael@0: michael@0: try { michael@0: let deferSaveComplete = Promise.defer(); michael@0: michael@0: if (this._canceled) { michael@0: // Don't create the BackgroundFileSaver object if we have been michael@0: // canceled meanwhile. michael@0: throw new DownloadError({ message: "Saver canceled." }); michael@0: } michael@0: michael@0: // Create the object that will save the file in a background thread. michael@0: let backgroundFileSaver = new BackgroundFileSaverStreamListener(); michael@0: try { michael@0: // When the operation completes, reflect the status in the promise michael@0: // returned by this download execution function. michael@0: backgroundFileSaver.observer = { michael@0: onTargetChange: function () { }, michael@0: onSaveComplete: (aSaver, aStatus) => { michael@0: // Send notifications now that we can restart if needed. michael@0: if (Components.isSuccessCode(aStatus)) { michael@0: // Save the hash before freeing backgroundFileSaver. michael@0: this._sha256Hash = aSaver.sha256Hash; michael@0: this._signatureInfo = aSaver.signatureInfo; michael@0: deferSaveComplete.resolve(); michael@0: } else { michael@0: // Infer the origin of the error from the failure code, because michael@0: // BackgroundFileSaver does not provide more specific data. michael@0: let properties = { result: aStatus, inferCause: true }; michael@0: deferSaveComplete.reject(new DownloadError(properties)); michael@0: } michael@0: // Free the reference cycle, to release resources earlier. michael@0: backgroundFileSaver.observer = null; michael@0: this._backgroundFileSaver = null; michael@0: }, michael@0: }; michael@0: michael@0: // Create a channel from the source, and listen to progress michael@0: // notifications. michael@0: let channel = NetUtil.newChannel(NetUtil.newURI(download.source.url)); michael@0: if (channel instanceof Ci.nsIPrivateBrowsingChannel) { michael@0: channel.setPrivate(download.source.isPrivate); michael@0: } michael@0: if (channel instanceof Ci.nsIHttpChannel && michael@0: download.source.referrer) { michael@0: channel.referrer = NetUtil.newURI(download.source.referrer); michael@0: } michael@0: michael@0: // If we have data that we can use to resume the download from where michael@0: // it stopped, try to use it. michael@0: let resumeAttempted = false; michael@0: let resumeFromBytes = 0; michael@0: if (channel instanceof Ci.nsIResumableChannel && this.entityID && michael@0: partFilePath && keepPartialData) { michael@0: try { michael@0: let stat = yield OS.File.stat(partFilePath); michael@0: channel.resumeAt(stat.size, this.entityID); michael@0: resumeAttempted = true; michael@0: resumeFromBytes = stat.size; michael@0: } catch (ex if ex instanceof OS.File.Error && michael@0: ex.becauseNoSuchFile) { } michael@0: } michael@0: michael@0: channel.notificationCallbacks = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]), michael@0: getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]), michael@0: onProgress: function DCSE_onProgress(aRequest, aContext, aProgress, michael@0: aProgressMax) michael@0: { michael@0: let currentBytes = resumeFromBytes + aProgress; michael@0: let totalBytes = aProgressMax == -1 ? -1 : (resumeFromBytes + michael@0: aProgressMax); michael@0: aSetProgressBytesFn(currentBytes, totalBytes, aProgress > 0 && michael@0: partFilePath && keepPartialData); michael@0: }, michael@0: onStatus: function () { }, michael@0: }; michael@0: michael@0: // Open the channel, directing output to the background file saver. michael@0: backgroundFileSaver.QueryInterface(Ci.nsIStreamListener); michael@0: channel.asyncOpen({ michael@0: onStartRequest: function (aRequest, aContext) { michael@0: backgroundFileSaver.onStartRequest(aRequest, aContext); michael@0: michael@0: // Check if the request's response has been blocked by Windows michael@0: // Parental Controls with an HTTP 450 error code. michael@0: if (aRequest instanceof Ci.nsIHttpChannel && michael@0: aRequest.responseStatus == 450) { michael@0: // Set a flag that can be retrieved later when handling the michael@0: // cancellation so that the proper error can be thrown. michael@0: this.download._blockedByParentalControls = true; michael@0: aRequest.cancel(Cr.NS_BINDING_ABORTED); michael@0: return; michael@0: } michael@0: michael@0: aSetPropertiesFn({ contentType: channel.contentType }); michael@0: michael@0: // Ensure we report the value of "Content-Length", if available, michael@0: // even if the download doesn't generate any progress events michael@0: // later. michael@0: if (channel.contentLength >= 0) { michael@0: aSetProgressBytesFn(0, channel.contentLength); michael@0: } michael@0: michael@0: // If the URL we are downloading from includes a file extension michael@0: // that matches the "Content-Encoding" header, for example ".gz" michael@0: // with a "gzip" encoding, we should save the file in its encoded michael@0: // form. In all other cases, we decode the body while saving. michael@0: if (channel instanceof Ci.nsIEncodedChannel && michael@0: channel.contentEncodings) { michael@0: let uri = channel.URI; michael@0: if (uri instanceof Ci.nsIURL && uri.fileExtension) { michael@0: // Only the first, outermost encoding is considered. michael@0: let encoding = channel.contentEncodings.getNext(); michael@0: if (encoding) { michael@0: channel.applyConversion = michael@0: gExternalHelperAppService.applyDecodingForExtension( michael@0: uri.fileExtension, encoding); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (keepPartialData) { michael@0: // If the source is not resumable, don't keep partial data even michael@0: // if we were asked to try and do it. michael@0: if (aRequest instanceof Ci.nsIResumableChannel) { michael@0: try { michael@0: // If reading the ID succeeds, the source is resumable. michael@0: this.entityID = aRequest.entityID; michael@0: } catch (ex if ex instanceof Components.Exception && michael@0: ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { michael@0: keepPartialData = false; michael@0: } michael@0: } else { michael@0: keepPartialData = false; michael@0: } michael@0: } michael@0: michael@0: // Enable hashing and signature verification before setting the michael@0: // target. michael@0: backgroundFileSaver.enableSha256(); michael@0: backgroundFileSaver.enableSignatureInfo(); michael@0: if (partFilePath) { michael@0: // If we actually resumed a request, append to the partial data. michael@0: if (resumeAttempted) { michael@0: // TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED michael@0: backgroundFileSaver.enableAppend(); michael@0: } michael@0: michael@0: // Use a part file, determining if we should keep it on failure. michael@0: backgroundFileSaver.setTarget(new FileUtils.File(partFilePath), michael@0: keepPartialData); michael@0: } else { michael@0: // Set the final target file, and delete it on failure. michael@0: backgroundFileSaver.setTarget(new FileUtils.File(targetPath), michael@0: false); michael@0: } michael@0: }.bind(copySaver), michael@0: michael@0: onStopRequest: function (aRequest, aContext, aStatusCode) { michael@0: try { michael@0: backgroundFileSaver.onStopRequest(aRequest, aContext, michael@0: aStatusCode); michael@0: } finally { michael@0: // If the data transfer completed successfully, indicate to the michael@0: // background file saver that the operation can finish. If the michael@0: // data transfer failed, the saver has been already stopped. michael@0: if (Components.isSuccessCode(aStatusCode)) { michael@0: if (partFilePath) { michael@0: // Move to the final target if we were using a part file. michael@0: backgroundFileSaver.setTarget( michael@0: new FileUtils.File(targetPath), false); michael@0: } michael@0: backgroundFileSaver.finish(Cr.NS_OK); michael@0: } michael@0: } michael@0: }.bind(copySaver), michael@0: michael@0: onDataAvailable: function (aRequest, aContext, aInputStream, michael@0: aOffset, aCount) { michael@0: backgroundFileSaver.onDataAvailable(aRequest, aContext, michael@0: aInputStream, aOffset, michael@0: aCount); michael@0: }.bind(copySaver), michael@0: }, null); michael@0: michael@0: // We should check if we have been canceled in the meantime, after michael@0: // all the previous asynchronous operations have been executed and michael@0: // just before we set the _backgroundFileSaver property. michael@0: if (this._canceled) { michael@0: throw new DownloadError({ message: "Saver canceled." }); michael@0: } michael@0: michael@0: // If the operation succeeded, store the object to allow cancellation. michael@0: this._backgroundFileSaver = backgroundFileSaver; michael@0: } catch (ex) { michael@0: // In case an error occurs while setting up the chain of objects for michael@0: // the download, ensure that we release the resources of the saver. michael@0: backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE); michael@0: throw ex; michael@0: } michael@0: michael@0: // We will wait on this promise in case no error occurred while setting michael@0: // up the chain of objects for the download. michael@0: yield deferSaveComplete.promise; michael@0: } catch (ex) { michael@0: // Ensure we always remove the placeholder for the final target file on michael@0: // failure, independently of which code path failed. In some cases, the michael@0: // background file saver may have already removed the file. michael@0: try { michael@0: yield OS.File.remove(targetPath); michael@0: } catch (e2) { michael@0: // If we failed during the operation, we report the error but use the michael@0: // original one as the failure reason of the download. Note that on michael@0: // Windows we may get an access denied error instead of a no such file michael@0: // error if the file existed before, and was recently deleted. michael@0: if (!(e2 instanceof OS.File.Error && michael@0: (e2.becauseNoSuchFile || e2.becauseAccessDenied))) { michael@0: Cu.reportError(e2); michael@0: } michael@0: } michael@0: throw ex; michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.cancel". michael@0: */ michael@0: cancel: function DCS_cancel() michael@0: { michael@0: this._canceled = true; michael@0: if (this._backgroundFileSaver) { michael@0: this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE); michael@0: this._backgroundFileSaver = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.removePartialData". michael@0: */ michael@0: removePartialData: function () michael@0: { michael@0: return Task.spawn(function task_DCS_removePartialData() { michael@0: if (this.download.target.partFilePath) { michael@0: try { michael@0: yield OS.File.remove(this.download.target.partFilePath); michael@0: } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { } michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.toSerializable". michael@0: */ michael@0: toSerializable: function () michael@0: { michael@0: // Simplify the representation if we don't have other details. michael@0: if (!this.entityID && !this._unknownProperties) { michael@0: return "copy"; michael@0: } michael@0: michael@0: let serializable = { type: "copy", michael@0: entityID: this.entityID }; michael@0: serializeUnknownProperties(this, serializable); michael@0: return serializable; michael@0: }, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.getSha256Hash" michael@0: */ michael@0: getSha256Hash: function () michael@0: { michael@0: return this._sha256Hash; michael@0: }, michael@0: michael@0: /* michael@0: * Implements DownloadSaver.getSignatureInfo. michael@0: */ michael@0: getSignatureInfo: function () michael@0: { michael@0: return this._signatureInfo; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creates a new DownloadCopySaver object, with its initial state derived from michael@0: * its serializable representation. michael@0: * michael@0: * @param aSerializable michael@0: * Serializable representation of a DownloadCopySaver object. michael@0: * michael@0: * @return The newly created DownloadCopySaver object. michael@0: */ michael@0: this.DownloadCopySaver.fromSerializable = function (aSerializable) { michael@0: let saver = new DownloadCopySaver(); michael@0: if ("entityID" in aSerializable) { michael@0: saver.entityID = aSerializable.entityID; michael@0: } michael@0: michael@0: deserializeUnknownProperties(saver, aSerializable, property => michael@0: property != "entityID" && property != "type"); michael@0: michael@0: return saver; michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadLegacySaver michael@0: michael@0: /** michael@0: * Saver object that integrates with the legacy nsITransfer interface. michael@0: * michael@0: * For more background on the process, see the DownloadLegacyTransfer object. michael@0: */ michael@0: this.DownloadLegacySaver = function() michael@0: { michael@0: this.deferExecuted = Promise.defer(); michael@0: this.deferCanceled = Promise.defer(); michael@0: } michael@0: michael@0: this.DownloadLegacySaver.prototype = { michael@0: __proto__: DownloadSaver.prototype, michael@0: michael@0: /** michael@0: * Save the SHA-256 hash in raw bytes of the downloaded file. This may be michael@0: * null when nsExternalHelperAppService (and thus BackgroundFileSaver) is not michael@0: * invoked. michael@0: */ michael@0: _sha256Hash: null, michael@0: michael@0: /** michael@0: * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert michael@0: * if the file is signed. This is empty if the file is unsigned, and null michael@0: * unless BackgroundFileSaver has successfully completed saving the file. michael@0: */ michael@0: _signatureInfo: null, michael@0: michael@0: /** michael@0: * nsIRequest object associated to the status and progress updates we michael@0: * received. This object is null before we receive the first status and michael@0: * progress update, and is also reset to null when the download is stopped. michael@0: */ michael@0: request: null, michael@0: michael@0: /** michael@0: * This deferred object contains a promise that is resolved as soon as this michael@0: * download finishes successfully, and is rejected in case the download is michael@0: * canceled or receives a failure notification through nsITransfer. michael@0: */ michael@0: deferExecuted: null, michael@0: michael@0: /** michael@0: * This deferred object contains a promise that is resolved if the download michael@0: * receives a cancellation request through the "cancel" method, and is never michael@0: * rejected. The nsITransfer implementation will register a handler that michael@0: * actually causes the download cancellation. michael@0: */ michael@0: deferCanceled: null, michael@0: michael@0: /** michael@0: * This is populated with the value of the aSetProgressBytesFn argument of the michael@0: * "execute" method, and is null before the method is called. michael@0: */ michael@0: setProgressBytesFn: null, michael@0: michael@0: /** michael@0: * Called by the nsITransfer implementation while the download progresses. michael@0: * michael@0: * @param aCurrentBytes michael@0: * Number of bytes transferred until now. michael@0: * @param aTotalBytes michael@0: * Total number of bytes to be transferred, or -1 if unknown. michael@0: */ michael@0: onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes) michael@0: { michael@0: // Ignore progress notifications until we are ready to process them. michael@0: if (!this.setProgressBytesFn) { michael@0: return; michael@0: } michael@0: michael@0: let hasPartFile = !!this.download.target.partFilePath; michael@0: michael@0: this.progressWasNotified = true; michael@0: this.setProgressBytesFn(aCurrentBytes, aTotalBytes, michael@0: aCurrentBytes > 0 && hasPartFile); michael@0: }, michael@0: michael@0: /** michael@0: * Whether the onProgressBytes function has been called at least once. michael@0: */ michael@0: progressWasNotified: false, michael@0: michael@0: /** michael@0: * Called by the nsITransfer implementation when the request has started. michael@0: * michael@0: * @param aRequest michael@0: * nsIRequest associated to the status update. michael@0: * @param aAlreadyAddedToHistory michael@0: * Indicates that the nsIExternalHelperAppService component already michael@0: * added the download to the browsing history, unless it was started michael@0: * from a private browsing window. When this parameter is false, the michael@0: * download is added to the browsing history here. Private downloads michael@0: * are never added to history even if this parameter is false. michael@0: */ michael@0: onTransferStarted: function (aRequest, aAlreadyAddedToHistory) michael@0: { michael@0: // Store the entity ID to use for resuming if required. michael@0: if (this.download.tryToKeepPartialData && michael@0: aRequest instanceof Ci.nsIResumableChannel) { michael@0: try { michael@0: // If reading the ID succeeds, the source is resumable. michael@0: this.entityID = aRequest.entityID; michael@0: } catch (ex if ex instanceof Components.Exception && michael@0: ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { } michael@0: } michael@0: michael@0: // For legacy downloads, we must update the referrer at this time. michael@0: if (aRequest instanceof Ci.nsIHttpChannel && aRequest.referrer) { michael@0: this.download.source.referrer = aRequest.referrer.spec; michael@0: } michael@0: michael@0: if (!aAlreadyAddedToHistory) { michael@0: this.addToHistory(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called by the nsITransfer implementation when the request has finished. michael@0: * michael@0: * @param aRequest michael@0: * nsIRequest associated to the status update. michael@0: * @param aStatus michael@0: * Status code received by the nsITransfer implementation. michael@0: */ michael@0: onTransferFinished: function DLS_onTransferFinished(aRequest, aStatus) michael@0: { michael@0: // Store a reference to the request, used when handling completion. michael@0: this.request = aRequest; michael@0: michael@0: if (Components.isSuccessCode(aStatus)) { michael@0: this.deferExecuted.resolve(); michael@0: } else { michael@0: // Infer the origin of the error from the failure code, because more michael@0: // specific data is not available through the nsITransfer implementation. michael@0: let properties = { result: aStatus, inferCause: true }; michael@0: this.deferExecuted.reject(new DownloadError(properties)); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * When the first execution of the download finished, it can be restarted by michael@0: * using a DownloadCopySaver object instead of the original legacy component michael@0: * that executed the download. michael@0: */ michael@0: firstExecutionFinished: false, michael@0: michael@0: /** michael@0: * In case the download is restarted after the first execution finished, this michael@0: * property contains a reference to the DownloadCopySaver that is executing michael@0: * the new download attempt. michael@0: */ michael@0: copySaver: null, michael@0: michael@0: /** michael@0: * String corresponding to the entityID property of the nsIResumableChannel michael@0: * used to execute the download, or null if the channel was not resumable or michael@0: * the saver was instructed not to keep partially downloaded data. michael@0: */ michael@0: entityID: null, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.execute". michael@0: */ michael@0: execute: function DLS_execute(aSetProgressBytesFn) michael@0: { michael@0: // Check if this is not the first execution of the download. The Download michael@0: // object guarantees that this function is not re-entered during execution. michael@0: if (this.firstExecutionFinished) { michael@0: if (!this.copySaver) { michael@0: this.copySaver = new DownloadCopySaver(); michael@0: this.copySaver.download = this.download; michael@0: this.copySaver.entityID = this.entityID; michael@0: this.copySaver.alreadyAddedToHistory = true; michael@0: } michael@0: return this.copySaver.execute.apply(this.copySaver, arguments); michael@0: } michael@0: michael@0: this.setProgressBytesFn = aSetProgressBytesFn; michael@0: michael@0: return Task.spawn(function task_DLS_execute() { michael@0: try { michael@0: // Wait for the component that executes the download to finish. michael@0: yield this.deferExecuted.promise; michael@0: michael@0: // At this point, the "request" property has been populated. Ensure we michael@0: // report the value of "Content-Length", if available, even if the michael@0: // download didn't generate any progress events. michael@0: if (!this.progressWasNotified && michael@0: this.request instanceof Ci.nsIChannel && michael@0: this.request.contentLength >= 0) { michael@0: aSetProgressBytesFn(0, this.request.contentLength); michael@0: } michael@0: michael@0: // If the component executing the download provides the path of a michael@0: // ".part" file, it means that it expects the listener to move the file michael@0: // to its final target path when the download succeeds. In this case, michael@0: // an empty ".part" file is created even if no data was received from michael@0: // the source. michael@0: if (this.download.target.partFilePath) { michael@0: yield OS.File.move(this.download.target.partFilePath, michael@0: this.download.target.path); michael@0: } else { michael@0: // The download implementation may not have created the target file if michael@0: // no data was received from the source. In this case, ensure that an michael@0: // empty file is created as expected. michael@0: try { michael@0: // This atomic operation is more efficient than an existence check. michael@0: let file = yield OS.File.open(this.download.target.path, michael@0: { create: true }); michael@0: yield file.close(); michael@0: } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { } michael@0: } michael@0: } catch (ex) { michael@0: // Ensure we always remove the final target file on failure, michael@0: // independently of which code path failed. In some cases, the michael@0: // component executing the download may have already removed the file. michael@0: try { michael@0: yield OS.File.remove(this.download.target.path); michael@0: } catch (e2) { michael@0: // If we failed during the operation, we report the error but use the michael@0: // original one as the failure reason of the download. Note that on michael@0: // Windows we may get an access denied error instead of a no such file michael@0: // error if the file existed before, and was recently deleted. michael@0: if (!(e2 instanceof OS.File.Error && michael@0: (e2.becauseNoSuchFile || e2.becauseAccessDenied))) { michael@0: Cu.reportError(e2); michael@0: } michael@0: } michael@0: // In case the operation failed, ensure we stop downloading data. Since michael@0: // we never re-enter this function, deferCanceled is always available. michael@0: this.deferCanceled.resolve(); michael@0: throw ex; michael@0: } finally { michael@0: // We don't need the reference to the request anymore. We must also set michael@0: // deferCanceled to null in order to free any indirect references it michael@0: // may hold to the request. michael@0: this.request = null; michael@0: this.deferCanceled = null; michael@0: // Allow the download to restart through a DownloadCopySaver. michael@0: this.firstExecutionFinished = true; michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.cancel". michael@0: */ michael@0: cancel: function DLS_cancel() michael@0: { michael@0: // We may be using a DownloadCopySaver to handle resuming. michael@0: if (this.copySaver) { michael@0: return this.copySaver.cancel.apply(this.copySaver, arguments); michael@0: } michael@0: michael@0: // If the download hasn't stopped already, resolve deferCanceled so that the michael@0: // operation is canceled as soon as a cancellation handler is registered. michael@0: // Note that the handler might not have been registered yet. michael@0: if (this.deferCanceled) { michael@0: this.deferCanceled.resolve(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.removePartialData". michael@0: */ michael@0: removePartialData: function () michael@0: { michael@0: // DownloadCopySaver and DownloadLeagcySaver use the same logic for removing michael@0: // partially downloaded data, though this implementation isn't shared by michael@0: // other saver types, thus it isn't found on their shared prototype. michael@0: return DownloadCopySaver.prototype.removePartialData.call(this); michael@0: }, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.toSerializable". michael@0: */ michael@0: toSerializable: function () michael@0: { michael@0: // This object depends on legacy components that are created externally, michael@0: // thus it cannot be rebuilt during deserialization. To support resuming michael@0: // across different browser sessions, this object is transformed into a michael@0: // DownloadCopySaver for the purpose of serialization. michael@0: return DownloadCopySaver.prototype.toSerializable.call(this); michael@0: }, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.getSha256Hash". michael@0: */ michael@0: getSha256Hash: function () michael@0: { michael@0: if (this.copySaver) { michael@0: return this.copySaver.getSha256Hash(); michael@0: } michael@0: return this._sha256Hash; michael@0: }, michael@0: michael@0: /** michael@0: * Called by the nsITransfer implementation when the hash is available. michael@0: */ michael@0: setSha256Hash: function (hash) michael@0: { michael@0: this._sha256Hash = hash; michael@0: }, michael@0: michael@0: /** michael@0: * Implements "DownloadSaver.getSignatureInfo". michael@0: */ michael@0: getSignatureInfo: function () michael@0: { michael@0: if (this.copySaver) { michael@0: return this.copySaver.getSignatureInfo(); michael@0: } michael@0: return this._signatureInfo; michael@0: }, michael@0: michael@0: /** michael@0: * Called by the nsITransfer implementation when the hash is available. michael@0: */ michael@0: setSignatureInfo: function (signatureInfo) michael@0: { michael@0: this._signatureInfo = signatureInfo; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Returns a new DownloadLegacySaver object. This saver type has a michael@0: * deserializable form only when creating a new object in memory, because it michael@0: * cannot be serialized to disk. michael@0: */ michael@0: this.DownloadLegacySaver.fromSerializable = function () { michael@0: return new DownloadLegacySaver(); michael@0: };