Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


SSL DownloadCore.sys.mjs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * Main implementation of the Downloads API objects. Consumers should get
 * references to these objects through the "Downloads.sys.mjs" module.
 */

import { Integration } from "resource://gre/modules/Integration.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  DownloadHistory: "resource://gre/modules/DownloadHistory.sys.mjs",
  DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "gExternalAppLauncher",
  "@mozilla.org/uriloader/external-helper-app-service;1",
  Ci.nsPIExternalAppLauncher
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "gExternalHelperAppService",
  "@mozilla.org/uriloader/external-helper-app-service;1",
  Ci.nsIExternalHelperAppService
);

Integration.downloads.defineESModuleGetter(
  lazy,
  "DownloadIntegration",
  "resource://gre/modules/DownloadIntegration.sys.mjs"
);

const BackgroundFileSaverStreamListener = Components.Constructor(
  "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
  "nsIBackgroundFileSaver"
);

/**
 * Returns true if the given value is a primitive string or a String object.
 */
function isString(aValue) {
  // We cannot use the "instanceof" operator reliably across module boundaries.
  return (
    typeof aValue == "string" ||
    (typeof aValue == "object" && "charAt" in aValue)
  );
}

/**
 * Serialize the unknown properties of aObject into aSerializable.
 */
function serializeUnknownProperties(aObject, aSerializable) {
  if (aObject._unknownProperties) {
    for (let property in aObject._unknownProperties) {
      aSerializable[property] = aObject._unknownProperties[property];
    }
  }
}

/**
 * Check for any unknown properties in aSerializable and preserve those in the
 * _unknownProperties field of aObject. aFilterFn is called for each property
 * name of aObject and should return true only for unknown properties.
 */
function deserializeUnknownProperties(aObject, aSerializable, aFilterFn) {
  for (let property in aSerializable) {
    if (aFilterFn(property)) {
      if (!aObject._unknownProperties) {
        aObject._unknownProperties = {};
      }

      aObject._unknownProperties[property] = aSerializable[property];
    }
  }
}

/**
 * Check if the file is a placeholder.
 *
 * @return {Promise}
 * @resolves {boolean}
 * @rejects Never.
 */
async function isPlaceholder(path) {
  try {
    if ((await IOUtils.stat(path)).size == 0) {
      return true;
    }
  } catch (ex) {
    // Canceling the download may have removed the placeholder already.
    if (ex.name != "NotFoundError") {
      console.error(ex);
    }
  }
  return false;
}

/**
 * This determines the minimum time interval between updates to the number of
 * bytes transferred, and is a limiting factor to the sequence of readings used
 * in calculating the speed of the download.
 */
const kProgressUpdateIntervalMs = 400;

/**
 * Represents a single download, with associated state and actions.  This object
 * is transient, though it can be included in a DownloadList so that it can be
 * managed by the user interface and persisted across sessions.
 */
export var Download = function () {
  this._deferSucceeded = Promise.withResolvers();
};

Download.prototype = {
  /**
   * DownloadSource object associated with this download.
   */
  source: null,

  /**
   * DownloadTarget object associated with this download.
   */
  target: null,

  /**
   * DownloadSaver object associated with this download.
   */
  saver: null,

  /**
   * Indicates that the download never started, has been completed successfully,
   * failed, or has been canceled.  This property becomes false when a download
   * is started for the first time, or when a failed or canceled download is
   * restarted.
   */
  stopped: true,

  /**
   * Indicates that the download has been completed successfully.
   */
  succeeded: false,

  /**
   * Indicates that the download has been canceled.  This property can become
   * true, then it can be reset to false when a canceled download is restarted.
   *
   * This property becomes true as soon as the "cancel" method is called, though
   * the "stopped" property might remain false until the cancellation request
   * has been processed.  Temporary files or part files may still exist even if
   * they are expected to be deleted, until the "stopped" property becomes true.
   */
  canceled: false,

  /**
   * Downloaded files can be deleted from within Firefox, e.g. via the context
   * menu. Currently Firefox does not track file moves (see bug 1746386), so if
   * a download's target file stops existing we have to assume it's "moved or
   * missing." To distinguish files intentionally deleted within Firefox from
   * files that are moved/missing, we mark them as "deleted" with this property.
   */
  deleted: false,

  /**
   * When the download fails, this is set to a DownloadError instance indicating
   * the cause of the failure.  If the download has been completed successfully
   * or has been canceled, this property is null.  This property is reset to
   * null when a failed download is restarted.
   */
  error: null,

  /**
   * Indicates the start time of the download.  When the download starts,
   * this property is set to a valid Date object.  The default value is null
   * before the download starts.
   */
  startTime: null,

  /**
   * Indicates whether this download's "progress" property is able to report
   * partial progress while the download proceeds, and whether the value in
   * totalBytes is relevant.  This depends on the saver and the download source.
   */
  hasProgress: false,

  /**
   * Progress percent, from 0 to 100.  Intermediate values are reported only if
   * hasProgress is true.
   *
   * @note You shouldn't rely on this property being equal to 100 to determine
   *       whether the download is completed.  You should use the individual
   *       state properties instead.
   */
  progress: 0,

  /**
   * When hasProgress is true, indicates the total number of bytes to be
   * transferred before the download finishes, that can be zero for empty files.
   *
   * When hasProgress is false, this property is always zero.
   *
   * @note This property may be different than the final file size on disk for
   *       downloads that are encoded during the network transfer.  You can use
   *       the "size" property of the DownloadTarget object to get the actual
   *       size on disk once the download succeeds.
   */
  totalBytes: 0,

  /**
   * Number of bytes currently transferred.  This value starts at zero, and may
   * be updated regardless of the value of hasProgress.
   *
   * @note You shouldn't rely on this property being equal to totalBytes to
   *       determine whether the download is completed.  You should use the
   *       individual state properties instead.  This property may not be
   *       updated during the last part of the download.
   */
  currentBytes: 0,

  /**
   * Fractional number representing the speed of the download, in bytes per
   * second.  This value is zero when the download is stopped, and may be
   * updated regardless of the value of hasProgress.
   */
  speed: 0,

  /**
   * Indicates whether, at this time, there is any partially downloaded data
   * that can be used when restarting a failed or canceled download.
   *
   * Even if the download has partial data on disk, hasPartialData will be false
   * if that data cannot be used to restart the download. In order to determine
   * if a part file is being used which contains partial data the
   * Download.target.partFilePath should be checked.
   *
   * This property is relevant while the download is in progress, and also if it
   * failed or has been canceled.  If the download has been completed
   * successfully, this property is always false.
   *
   * Whether partial data can actually be retained depends on the saver and the
   * download source, and may not be known before the download is started.
   */
  hasPartialData: false,

  /**
   * Indicates whether, at this time, there is any data that has been blocked.
   * Since reputation blocking takes place after the download has fully
   * completed a value of true also indicates 100% of the data is present.
   */
  hasBlockedData: false,

  /**
   * This can be set to a function that is called after other properties change.
   */
  onchange: null,

  /**
   * This tells if the user has chosen to open/run the downloaded file after
   * download has completed.
   */
  launchWhenSucceeded: false,

  /**
   * When a download starts, we typically want to automatically open the
   * downloads panel if the pref browser.download.alwaysOpenPanel is enabled.
   * However, there are conditions where we want to prevent this. For example, a
   * false value can prevent the downloads panel from opening when an add-on
   * creates a download without user input as part of some background operation.
   */
  openDownloadsListOnStart: true,

  /**
   * This represents the MIME type of the download.
   */
  contentType: null,

  /**
   * This indicates the path of the application to be used to launch the file,
   * or null if the file should be launched with the default application.
   */
  launcherPath: null,

  /**
   * This contains application id to be used to launch the file,
   * or null if the file is not meant to be launched with GIOHandlerApp.
   */
  launcherId: null,

  /**
   * Raises the onchange notification.
   */
  _notifyChange: function D_notifyChange() {
    try {
      if (this.onchange) {
        this.onchange();
      }
    } catch (ex) {
      console.error(ex);
    }
  },

  /**
   * The download may be stopped and restarted multiple times before it
   * completes successfully. This may happen if any of the download attempts is
   * canceled or fails.
   *
   * This property contains a promise that is linked to the current attempt, or
   * null if the download is either stopped or in the process of being canceled.
   * If the download restarts, this property is replaced with a new promise.
   *
   * The promise is resolved if the attempt it represents finishes successfully,
   * and rejected if the attempt fails.
   */
  _currentAttempt: null,

  /**
   * The download was launched to open from the Downloads Panel.
   */
  _launchedFromPanel: false,

  /**
   * Starts the download for the first time, or restarts a download that failed
   * or has been canceled.
   *
   * Calling this method when the download has been completed successfully has
   * no effect, and the method returns a resolved promise.  If the download is
   * in progress, the method returns the same promise as the previous call.
   *
   * If the "cancel" method was called but the cancellation process has not
   * finished yet, this method waits for the cancellation to finish, then
   * restarts the download immediately.
   *
   * @note If you need to start a new download from the same source, rather than
   *       restarting a failed or canceled one, you should create a separate
   *       Download object with the same source as the current one.
   *
   * @return {Promise}
   * @resolves When the download has finished successfully.
   * @rejects JavaScript exception if the download failed.
   */
  start: function D_start() {
    // If the download succeeded, it's the final state, we have nothing to do.
    if (this.succeeded) {
      return Promise.resolve();
    }

    // If the download already started and hasn't failed or hasn't been
    // canceled, return the same promise as the previous call, allowing the
    // caller to wait for the current attempt to finish.
    if (this._currentAttempt) {
      return this._currentAttempt;
    }

    // While shutting down or disposing of this object, we prevent the download
    // from returning to be in progress.
    if (this._finalized) {
      return Promise.reject(
        new DownloadError({
          message: "Cannot start after finalization.",
        })
      );
    }

    if (this.error && this.error.becauseBlockedByReputationCheck) {
      return Promise.reject(
        new DownloadError({
          message: "Cannot start after being blocked by a reputation check.",
        })
      );
    }

    // Initialize all the status properties for a new or restarted download.
    this.stopped = false;
    this.canceled = false;
    this.error = null;
    // Avoid serializing the previous error, or it would be restored on the next
    // startup, even if the download was restarted.
    delete this._unknownProperties?.errorObj;
    this.hasProgress = false;
    this.hasBlockedData = false;
    this.progress = 0;
    this.totalBytes = 0;
    this.currentBytes = 0;
    this.startTime = new Date();

    // Create a new deferred object and an associated promise before starting
    // the actual download.  We store it on the download as the current attempt.
    let deferAttempt = Promise.withResolvers();
    let currentAttempt = deferAttempt.promise;
    this._currentAttempt = currentAttempt;

    // Restart the progress and speed calculations from scratch.
    this._lastProgressTimeMs = 0;

    // This function propagates progress from the DownloadSaver object, unless
    // it comes in late from a download attempt that was replaced by a new one.
    // If the cancellation process for the download has started, then the update
    // is ignored.
    function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
      if (this._currentAttempt == currentAttempt) {
        this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData);
      }
    }

    // This function propagates download properties from the DownloadSaver
    // object, unless it comes in late from a download attempt that was
    // replaced by a new one.  If the cancellation process for the download has
    // started, then the update is ignored.
    function DS_setProperties(aOptions) {
      if (this._currentAttempt != currentAttempt) {
        return;
      }

      let changeMade = false;

      for (let property of [
        "contentType",
        "progress",
        "hasPartialData",
        "hasBlockedData",
      ]) {
        if (property in aOptions && this[property] != aOptions[property]) {
          this[property] = aOptions[property];
          changeMade = true;
        }
      }

      if (changeMade) {
        this._notifyChange();
      }
    }

    // Now that we stored the promise in the download object, we can start the
    // task that will actually execute the download.
    deferAttempt.resolve(
      (async () => {
        // Wait upon any pending operation before restarting.
        if (this._promiseCanceled) {
          await this._promiseCanceled;
        }
        if (this._promiseRemovePartialData) {
          try {
            await this._promiseRemovePartialData;
          } catch (ex) {
            // Ignore any errors, which are already reported by the original
            // caller of the removePartialData method.
          }
        }

        // In case the download was restarted while cancellation was in progress,
        // but the previous attempt actually succeeded before cancellation could
        // be processed, it is possible that the download has already finished.
        if (this.succeeded) {
          return;
        }

        try {
          if (this.downloadingToSameFile()) {
            throw new DownloadError({
              message: "Can't overwrite the source file.",
              becauseTargetFailed: true,
            });
          }

          // Disallow download if parental controls service restricts it.
          if (
            await lazy.DownloadIntegration.shouldBlockForParentalControls(this)
          ) {
            throw new DownloadError({ becauseBlockedByParentalControls: true });
          }

          // We should check if we have been canceled in the meantime, after all
          // the previous asynchronous operations have been executed and just
          // before we call the "execute" method of the saver.
          if (this._promiseCanceled) {
            // The exception will become a cancellation in the "catch" block.
            throw new Error(undefined);
          }

          // Execute the actual download through the saver object.
          this._saverExecuting = true;
          try {
            await this.saver.execute(
              DS_setProgressBytes.bind(this),
              DS_setProperties.bind(this)
            );
          } catch (ex) {
            // Remove the target file placeholder and all partial data when
            // needed, independently of which code path failed. In some cases, the
            // component executing the download may have already removed the file.
            if (!this.hasPartialData && !this.hasBlockedData) {
              await this.saver.removeData(true);
            }
            throw ex;
          }

          // Now that the actual saving finished, read the actual file size on
          // disk, that may be different from the amount of data transferred.
          await this.target.refresh();

          // Check for the last time if the download has been canceled. This must
          // be done right before setting the "stopped" property of the download,
          // without any asynchronous operations in the middle, so that another
          // cancellation request cannot start in the meantime and stay unhandled.
          if (this._promiseCanceled) {
            // To keep the internal state of the Download object consistent, we
            // just delete the target and effectively cancel the download. Since
            // the DownloadSaver succeeded, we already renamed the ".part" file to
            // the final name, and this results in all the data being deleted.
            await this.saver.removeData(true);

            // Cancellation exceptions will be changed in the catch block below.
            throw new DownloadError();
          }

          // Update the status properties for a successful download.
          this.progress = 100;
          this.succeeded = true;
          this.hasPartialData = false;
        } catch (originalEx) {
          // We may choose a different exception to propagate in the code below,
          // or wrap the original one. We do this mutation in a different variable
          // because of the "no-ex-assign" ESLint rule.
          let ex = originalEx;

          // Fail with a generic status code on cancellation, so that the caller
          // is forced to actually check the status properties to see if the
          // download was canceled or failed because of other reasons.
          if (this._promiseCanceled) {
            throw new DownloadError({ message: "Download canceled." });
          }

          // An HTTP 450 error code is used by Windows to indicate that a uri is
          // blocked by parental controls. This will prevent the download from
          // occuring, so an error needs to be raised. This is not performed
          // during the parental controls check above as it requires the request
          // to start.
          if (this._blockedByParentalControls) {
            ex = new DownloadError({ becauseBlockedByParentalControls: true });
          }

          // Update the download error, unless a new attempt already started. The
          // change in the status property is notified in the finally block.
          if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
            if (!(ex instanceof DownloadError)) {
              let properties = { innerException: ex };

              if (ex.message) {
                properties.message = ex.message;
              }

              ex = new DownloadError(properties);
            }
            // Don't store an error if it's an abort caused by shutdown, so the
            // download can be retried automatically at the next startup.
            if (
              originalEx.result != Cr.NS_ERROR_ABORT ||
              !Services.startup.isInOrBeyondShutdownPhase(
                Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
              )
            ) {
              this.error = ex;
            }
          }
          throw ex;
        } finally {
          // Any cancellation request has now been processed.
          this._saverExecuting = false;
          this._promiseCanceled = null;

          // Update the status properties, unless a new attempt already started.
          if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
            this._currentAttempt = null;
            this.stopped = true;
            this.speed = 0;
            this._notifyChange();
            if (this.succeeded) {
              await this._succeed();
            }
          }
        }
      })()
    );

    // Notify the new download state before returning.
    this._notifyChange();
    return currentAttempt;
  },

  /**
   * Perform the actions necessary when a Download succeeds.
   *
   * @return {Promise}
   * @resolves When the steps to take after success have completed.
   * @rejects  JavaScript exception if any of the operations failed.
   */
  async _succeed() {
    await lazy.DownloadIntegration.downloadDone(this);

    this._deferSucceeded.resolve();

    if (this.launchWhenSucceeded) {
      this.launch().catch(console.error);

      // Always schedule files to be deleted at the end of the private browsing
      // mode, regardless of the value of the pref.
      if (this.source.isPrivate) {
        lazy.gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
          new lazy.FileUtils.File(this.target.path)
        );
      } else if (
        Services.prefs.getBoolPref("browser.helperApps.deleteTempFileOnExit") &&
        Services.prefs.getBoolPref(
          "browser.download.start_downloads_in_tmp_dir",
          false
        )
      ) {
        lazy.gExternalAppLauncher.deleteTemporaryFileOnExit(
          new lazy.FileUtils.File(this.target.path)
        );
      }
    }
  },

  /**
   * When a request to unblock the download is received, contains a promise
   * that will be resolved when the unblock request is completed. This property
   * will then continue to hold the promise indefinitely.
   */
  _promiseUnblock: null,

  /**
   * When a request to confirm the block of the download is received, contains
   * a promise that will be resolved when cleaning up the download has
   * completed. This property will then continue to hold the promise
   * indefinitely.
   */
  _promiseConfirmBlock: null,

  /**
   * Unblocks a download which had been blocked by reputation.
   *
   * The file will be moved out of quarantine and the download will be
   * marked as succeeded.
   *
   * @return {Promise}
   * @resolves When the Download has been unblocked and succeeded.
   * @rejects  JavaScript exception if any of the operations failed.
   */
  unblock() {
    if (this._promiseUnblock) {
      return this._promiseUnblock;
    }

    if (this._promiseConfirmBlock) {
      return Promise.reject(
        new Error("Download block has been confirmed, cannot unblock.")
      );
    }

    if (this.error?.becauseBlockedByReputationCheck) {
      Glean.downloads.userActionOnBlockedDownload[
        this.error.reputationCheckVerdict
      ].accumulateSingleSample(2); // unblock
    }

    if (
      this.error?.reputationCheckVerdict == DownloadError.BLOCK_VERDICT_INSECURE
    ) {
      // In this Error case, the download was actually canceled before it was
      // passed to the Download UI. So we need to start the download here.
      this.error = null;
      this.succeeded = false;
      this.hasBlockedData = false;
      // This ensures the verdict will not get set again after the browser
      // restarts and the download gets serialized and de-serialized again.
      delete this._unknownProperties?.errorObj;
      this.start()
        .catch(err => {
          if (err.becauseTargetFailed) {
            // In case we cannot write to the target file
            // retry with a new unique name
            let uniquePath = lazy.DownloadPaths.createNiceUniqueFile(
              new lazy.FileUtils.File(this.target.path)
            ).path;
            this.target.path = uniquePath;
            return this.start();
          }
          return Promise.reject(err);
        })
        .catch(err => {
          if (!this.canceled) {
            console.error(err);
          }
          this._notifyChange();
        });
      this._notifyChange();
      this._promiseUnblock = lazy.DownloadIntegration.downloadDone(this);
      return this._promiseUnblock;
    }

    if (!this.hasBlockedData) {
      return Promise.reject(
        new Error("unblock may only be called on Downloads with blocked data.")
      );
    }

    this._promiseUnblock = (async () => {
      try {
        await IOUtils.move(this.target.partFilePath, this.target.path);
        await this.target.refresh();
      } catch (ex) {
        await this.refresh();
        this._promiseUnblock = null;
        throw ex;
      }

      this.succeeded = true;
      this.hasBlockedData = false;
      this._notifyChange();
      await this._succeed();
    })();

    return this._promiseUnblock;
  },

  /**
   * Confirms that a blocked download should be cleaned up.
   *
   * If a download was blocked but retained on disk this method can be used
   * to remove the file.
   *
   * @return {Promise}
   * @resolves When the Download's data has been removed.
   * @rejects  JavaScript exception if any of the operations failed.
   */
  confirmBlock() {
    if (this._promiseConfirmBlock) {
      return this._promiseConfirmBlock;
    }

    if (this._promiseUnblock) {
      return Promise.reject(
        new Error("Download is being unblocked, cannot confirmBlock.")
      );
    }

    if (this.error?.becauseBlockedByReputationCheck) {
      // We have to record the telemetry in both DownloadsCommon.deleteDownload
      // and confirmBlock here. The former is for cases where users click
      // "Remove file" in the download panel and the latter is when
      // users click "X" button in about:downloads.
      Glean.downloads.userActionOnBlockedDownload[
        this.error.reputationCheckVerdict
      ].accumulateSingleSample(1); // confirm block
    }

    if (!this.hasBlockedData) {
      return Promise.reject(
        new Error(
          "confirmBlock may only be called on Downloads with blocked data."
        )
      );
    }

    this._promiseConfirmBlock = (async () => {
      // This call never throws exceptions. If the removal fails, the blocked
      // data remains stored on disk in the ".part" file.
      await this.saver.removeData();

      this.hasBlockedData = false;
      this._notifyChange();
    })();

    return this._promiseConfirmBlock;
  },

  /*
   * Launches the file after download has completed. This can open
   * the file with the default application for the target MIME type
   * or file extension, or with a custom application if launcherPath
   * or launcherId is set.
   *
   * @param options.openWhere  Optional string indicating how to open when handling
   *                           download by opening the target file URI.
   *                           One of "window", "tab", "tabshifted"
   * @param options.useSystemDefault
   *                           Optional value indicating how to handle launching this download,
   *                           this time only. Will override the associated mimeInfo.preferredAction
   * @return {Promise}
   * @resolves When the instruction to launch the file has been
   *           successfully given to the operating system. Note that
   *           the OS might still take a while until the file is actually
   *           launched.
   * @rejects  JavaScript exception if there was an error trying to launch
   *           the file.
   */
  launch(options = {}) {
    if (!this.succeeded) {
      return Promise.reject(
        new Error("launch can only be called if the download succeeded")
      );
    }

    if (this._launchedFromPanel) {
      Glean.downloads.fileOpened.add(1);
    }

    return lazy.DownloadIntegration.launchDownload(this, options);
  },

  /*
   * Shows the folder containing the target file, or where the target file
   * will be saved. This may be called at any time, even if the download
   * failed or is currently in progress.
   *
   * @return {Promise}
   * @resolves When the instruction to open the containing folder has been
   *           successfully given to the operating system. Note that
   *           the OS might still take a while until the folder is actually
   *           opened.
   * @rejects  JavaScript exception if there was an error trying to open
   *           the containing folder.
   */
  showContainingDirectory: function D_showContainingDirectory() {
    return lazy.DownloadIntegration.showContainingDirectory(this.target.path);
  },

  /**
   * When a request to cancel the download is received, contains a promise that
   * will be resolved when the cancellation request is processed.  When the
   * request is processed, this property becomes null again.
   */
  _promiseCanceled: null,

  /**
   * True between the call to the "execute" method of the saver and the
   * completion of the current download attempt.
   */
  _saverExecuting: false,

  /**
   * Cancels the download.
   *
   * The cancellation request is asynchronous.  Until the cancellation process
   * finishes, temporary files or part files may still exist even if they are
   * expected to be deleted.
   *
   * In case the download completes successfully before the cancellation request
   * could be processed, this method has no effect, and it returns a resolved
   * promise.  You should check the properties of the download at the time the
   * returned promise is resolved to determine if the download was cancelled.
   *
   * Calling this method when the download has been completed successfully,
   * failed, or has been canceled has no effect, and the method returns a
   * resolved promise.  This behavior is designed for the case where the call
   * to "cancel" happens asynchronously, and is consistent with the case where
   * the cancellation request could not be processed in time.
   *
   * @return {Promise}
   * @resolves When the cancellation process has finished.
   * @rejects Never.
   */
  cancel: function D_cancel() {
    // If the download is currently stopped, we have nothing to do.
    if (this.stopped) {
      return Promise.resolve();
    }

    if (!this._promiseCanceled) {
      // Start a new cancellation request.
      this._promiseCanceled = new Promise(resolve => {
        this._currentAttempt.then(resolve, resolve);
      });

      // The download can already be restarted.
      this._currentAttempt = null;

      // Notify that the cancellation request was received.
      this.canceled = true;
      this._notifyChange();

      // Execute the actual cancellation through the saver object, in case it
      // has already started.  Otherwise, the cancellation will be handled just
      // before the saver is started.
      if (this._saverExecuting) {
        this.saver.cancel();
      }
    }

    return this._promiseCanceled;
  },

  /**
   * Indicates whether any partially downloaded data should be retained, to use
   * when restarting a failed or canceled download.  The default is false.
   *
   * Whether partial data can actually be retained depends on the saver and the
   * download source, and may not be known before the download is started.
   *
   * To have any effect, this property must be set before starting the download.
   * Resetting this property to false after the download has already started
   * will not remove any partial data.
   *
   * If this property is set to true, care should be taken that partial data is
   * removed before the reference to the download is discarded.  This can be
   * done using the removePartialData or the "finalize" methods.
   */
  tryToKeepPartialData: false,

  /**
   * When a request to remove partially downloaded data is received, contains a
   * promise that will be resolved when the removal request is processed.  When
   * the request is processed, this property becomes null again.
   */
  _promiseRemovePartialData: null,

  /**
   * Removes any partial data kept as part of a canceled or failed download.
   *
   * If the download is not canceled or failed, this method has no effect, and
   * it returns a resolved promise.  If the "cancel" method was called but the
   * cancellation process has not finished yet, this method waits for the
   * cancellation to finish, then removes the partial data.
   *
   * After this method has been called, if the tryToKeepPartialData property is
   * still true when the download is restarted, partial data will be retained
   * during the new download attempt.
   *
   * @return {Promise}
   * @resolves When the partial data has been successfully removed.
   * @rejects JavaScript exception if the operation could not be completed.
   */
  removePartialData() {
    if (!this.canceled && !this.error) {
      return Promise.resolve();
    }

    if (!this._promiseRemovePartialData) {
      this._promiseRemovePartialData = (async () => {
        try {
          // Wait upon any pending cancellation request.
          if (this._promiseCanceled) {
            await this._promiseCanceled;
          }
          // Ask the saver object to remove any partial data.
          await this.saver.removeData();
          // For completeness, clear the number of bytes transferred.
          if (this.currentBytes != 0 || this.hasPartialData) {
            this.currentBytes = 0;
            this.hasPartialData = false;
            this.target.refreshPartFileState();
            this._notifyChange();
          }
        } finally {
          this._promiseRemovePartialData = null;
        }
      })();
    }

    return this._promiseRemovePartialData;
  },

  /**
   * Returns true if the download source is the same as the target file.
   */
  downloadingToSameFile() {
    if (!this.source.url || !this.source.url.startsWith("file:")) {
      return false;
    }

    try {
      let sourceUri = lazy.NetUtil.newURI(this.source.url);
      let targetUri = lazy.NetUtil.newURI(
        new lazy.FileUtils.File(this.target.path)
      );
      return sourceUri.equals(targetUri);
    } catch (ex) {
      return false;
    }
  },

  /**
   * This deferred object contains a promise that is resolved as soon as this
   * download finishes successfully, and is never rejected.  This property is
   * initialized when the download is created, and never changes.
   */
  _deferSucceeded: null,

  /**
   * Returns a promise that is resolved as soon as this download finishes
   * successfully, even if the download was stopped and restarted meanwhile.
   *
   * You can use this property for scheduling download completion actions in the
   * current session, for downloads that are controlled interactively.  If the
   * download is not controlled interactively, you should use the promise
   * returned by the "start" method instead, to check for success or failure.
   *
   * @return {Promise}
   * @resolves When the download has finished successfully.
   * @rejects Never.
   */
  whenSucceeded: function D_whenSucceeded() {
    return this._deferSucceeded.promise;
  },

  /**
   * Updates the state of a finished, failed, or canceled download based on the
   * current state in the file system.  If the download is in progress or it has
   * been finalized, this method has no effect, and it returns a resolved
   * promise.
   *
   * This allows the properties of the download to be updated in case the user
   * moved or deleted the target file or its associated ".part" file.
   *
   * @return {Promise}
   * @resolves When the operation has completed.
   * @rejects Never.
   */
  refresh() {
    return (async () => {
      if (!this.stopped || this._finalized) {
        return;
      }

      if (this.succeeded) {
        let oldExists = this.target.exists;
        let oldSize = this.target.size;
        await this.target.refresh();
        if (oldExists != this.target.exists || oldSize != this.target.size) {
          this._notifyChange();
        }
        return;
      }

      // Update the current progress from disk if we retained partial data.
      if (
        (this.hasPartialData || this.hasBlockedData) &&
        this.target.partFilePath
      ) {
        try {
          let stat = await IOUtils.stat(this.target.partFilePath);

          // Ignore the result if the state has changed meanwhile.
          if (!this.stopped || this._finalized) {
            return;
          }

          // Update the bytes transferred and the related progress properties.
          this.currentBytes = stat.size;
          if (this.totalBytes > 0) {
            this.hasProgress = true;
            this.progress = Math.floor(
              (this.currentBytes / this.totalBytes) * 100
            );
          }
        } catch (ex) {
          if (ex.name != "NotFoundError") {
            throw ex;
          }
          // Ignore the result if the state has changed meanwhile.
          if (!this.stopped || this._finalized) {
            return;
          }
          // In case we've blocked the Download becasue its
          // insecure, we should not set hasBlockedData to
          // false as its required to show the Unblock option.
          if (
            this.error.reputationCheckVerdict ==
            DownloadError.BLOCK_VERDICT_INSECURE
          ) {
            return;
          }

          this.hasBlockedData = false;
          this.hasPartialData = false;
        }

        this._notifyChange();
      }
    })().catch(console.error);
  },

  /**
   * True if the "finalize" method has been called.  This prevents the download
   * from starting again after having been stopped.
   */
  _finalized: false,

  /**
   * True if the "finalize" has been called and fully finished it's execution.
   */
  _finalizeExecuted: false,

  /**
   * Ensures that the download is stopped, and optionally removes any partial
   * data kept as part of a canceled or failed download.  After this method has
   * been called, the download cannot be started again.
   *
   * This method should be used in place of "cancel" and removePartialData while
   * shutting down or disposing of the download object, to prevent other callers
   * from interfering with the operation.  This is required because cancellation
   * and other operations are asynchronous.
   *
   * @param aRemovePartialData
   *        Whether any partially downloaded data should be removed after the
   *        download has been stopped.
   *
   * @return {Promise}
   * @resolves When the operation has finished successfully.
   * @rejects JavaScript exception if an error occurred while removing the
   *          partially downloaded data.
   */
  finalize(aRemovePartialData) {
    // Prevents the download from starting again after having been stopped.
    this._finalized = true;
    let promise;

    if (aRemovePartialData) {
      // Cancel the download, in case it is currently in progress, then remove
      // any partially downloaded data.  The removal operation waits for
      // cancellation to be completed before resolving the promise it returns.
      this.cancel();
      promise = this.removePartialData();
    } else {
      // Just cancel the download, in case it is currently in progress.
      promise = this.cancel();
    }
    promise.then(() => {
      // At this point, either removing data / just cancelling the download should be done.
      this._finalizeExecuted = true;
    });

    return promise;
  },

  /**
   * Deletes all file data associated with a download, preserving the download
   * object itself and updating it for download views.
   */
  async manuallyRemoveData() {
    let { path } = this.target;
    if (this.succeeded) {
      // Temp files are made "read-only" by DownloadIntegration.downloadDone, so
      // reset the permission bits to read/write. This won't be necessary after
      // bug 1733587 since Downloads won't ever be temporary.
      await IOUtils.setPermissions(path, 0o660);
      await IOUtils.remove(path, { ignoreAbsent: true });
    }
    this.deleted = true;
    await this.cancel();
    await this.removePartialData();
    // We need to guarantee that the UI is refreshed irrespective of what state
    // the download is in when this is called, to ensure the download doesn't
    // wind up stuck displaying as if it exists when it actually doesn't. And
    // that means updating this.target.partFileExists no matter what.
    await this.target.refreshPartFileState();
    await this.refresh();
    // The above methods will sometimes call _notifyChange, but not always. It
    // depends on whether the download is `succeeded`, `stopped`, `canceled`,
    // etc. Since this method needs to update the UI and can be invoked on any
    // download as long as its target has some file on the system, we need to
    // call _notifyChange no matter what state the download is in.
    this._notifyChange();
  },

  /**
   * Indicates the time of the last progress notification, expressed as the
   * number of milliseconds since January 1, 1970, 00:00:00 UTC.  This is zero
   * until some bytes have actually been transferred.
   */
  _lastProgressTimeMs: 0,

  /**
   * Updates progress notifications based on the number of bytes transferred.
   *
   * The number of bytes transferred is not updated unless enough time passed
   * since this function was last called.  This limits the computation load, in
   * particular when the listeners update the user interface in response.
   *
   * @param aCurrentBytes
   *        Number of bytes transferred until now.
   * @param aTotalBytes
   *        Total number of bytes to be transferred, or -1 if unknown.
   * @param [aHasPartialData]
   *        Indicates whether the partially downloaded data can be used when
   *        restarting the download if it fails or is canceled.
   */
  _setBytes: function D_setBytes(
    aCurrentBytes,
    aTotalBytes,
    aHasPartialData = false
  ) {
    let changeMade = this.hasPartialData != aHasPartialData;
    this.hasPartialData = aHasPartialData;

    // Unless aTotalBytes is -1, we can report partial download progress.  In
    // this case, notify when the related properties changed since last time.
    if (
      aTotalBytes != -1 &&
      (!this.hasProgress || this.totalBytes != aTotalBytes)
    ) {
      this.hasProgress = true;
      this.totalBytes = aTotalBytes;
      changeMade = true;
    }

    // Updating the progress and computing the speed require that enough time
    // passed since the last update, or that we haven't started throttling yet.
    let currentTimeMs = Date.now();
    let intervalMs = currentTimeMs - this._lastProgressTimeMs;
    if (intervalMs >= kProgressUpdateIntervalMs) {
      // Don't compute the speed unless we started throttling notifications.
      if (this._lastProgressTimeMs != 0) {
        // Calculate the speed in bytes per second.
        let rawSpeed =
          ((aCurrentBytes - this.currentBytes) / intervalMs) * 1000;
        if (this.speed == 0) {
          // When the previous speed is exactly zero instead of a fractional
          // number, this can be considered the first element of the series.
          this.speed = rawSpeed;
        } else {
          // Apply exponential smoothing, with a smoothing factor of 0.1.
          this.speed = rawSpeed * 0.1 + this.speed * 0.9;
        }
      }

      // Start throttling notifications only when we have actually received some
      // bytes for the first time.  The timing of the first part of the download
      // is not reliable, due to possible latency in the initial notifications.
      // This also allows automated tests to receive and verify the number of
      // bytes initially transferred.
      if (aCurrentBytes > 0) {
        this._lastProgressTimeMs = currentTimeMs;

        // Update the progress now that we don't need its previous value.
        this.currentBytes = aCurrentBytes;
        if (this.totalBytes > 0) {
          this.progress = Math.floor(
            (this.currentBytes / this.totalBytes) * 100
          );
        }
        changeMade = true;
      }

      if (this.hasProgress && this.target && !this.target.partFileExists) {
        this.target.refreshPartFileState();
      }
    }

    if (changeMade) {
      this._notifyChange();
    }
  },

  /**
   * Returns a static representation of the current object state.
   *
   * @return A JavaScript object that can be serialized to JSON.
   */
  toSerializable() {
    let serializable = {
      source: this.source.toSerializable(),
      target: this.target.toSerializable(),
    };

    let saver = this.saver.toSerializable();
    if (!serializable.source || !saver) {
      // If we are unable to serialize either the source or the saver,
      // we won't persist the download.
      return null;
    }

    // Simplify the representation for the most common saver type.  If the saver
    // is an object instead of a simple string, we can't simplify it because we
    // need to persist all its properties, not only "type".  This may happen for
    // savers of type "copy" as well as other types.
    if (saver !== "copy") {
      serializable.saver = saver;
    }

    if (this.error) {
      serializable.errorObj = this.error.toSerializable();
    }

    if (this.startTime) {
      serializable.startTime = this.startTime.toJSON();
    }

    // These are serialized unless they are false, null, or empty strings.
    for (let property of kPlainSerializableDownloadProperties) {
      if (this[property]) {
        serializable[property] = this[property];
      }
    }

    serializeUnknownProperties(this, serializable);

    return serializable;
  },

  /**
   * Returns a value that changes only when one of the properties of a Download
   * object that should be saved into a file also change.  This excludes
   * properties whose value doesn't usually change during the download lifetime.
   *
   * This function is used to determine whether the download should be
   * serialized after a property change notification has been received.
   *
   * @return String representing the relevant download state.
   */
  getSerializationHash() {
    // The "succeeded", "canceled", "error", and startTime properties are not
    // taken into account because they all change before the "stopped" property
    // changes, and are not altered in other cases.
    return (
      this.stopped +
      "," +
      this.totalBytes +
      "," +
      this.hasPartialData +
      "," +
      this.contentType
    );
  },
};

/**
 * Defines which properties of the Download object are serializable.
 */
const kPlainSerializableDownloadProperties = [
  "succeeded",
  "canceled",
  "totalBytes",
  "hasPartialData",
  "hasBlockedData",
  "tryToKeepPartialData",
  "launcherPath",
  "launcherId",
  "launchWhenSucceeded",
  "contentType",
  "handleInternally",
  "openDownloadsListOnStart",
];

/**
 * Creates a new Download object from a serializable representation.  This
 * function is used by the createDownload method of Downloads.sys.mjs when a new
 * Download object is requested, thus some properties may refer to live objects
 * in place of their serializable representations.
 *
 * @param aSerializable
 *        An object with the following fields:
 *        {
 *          source: DownloadSource object, or its serializable representation.
 *                  See DownloadSource.fromSerializable for details.
 *          target: DownloadTarget object, or its serializable representation.
 *                  See DownloadTarget.fromSerializable for details.
 *          saver: Serializable representation of a DownloadSaver object.  See
 *                 DownloadSaver.fromSerializable for details.  If omitted,
 *                 defaults to "copy".
 *        }
 *
 * @return The newly created Download object.
 */
Download.fromSerializable = function (aSerializable) {
  let download = new Download();
  if (aSerializable.source instanceof DownloadSource) {
    download.source = aSerializable.source;
  } else {
    download.source = DownloadSource.fromSerializable(aSerializable.source);
  }
  if (aSerializable.target instanceof DownloadTarget) {
    download.target = aSerializable.target;
  } else {
    download.target = DownloadTarget.fromSerializable(aSerializable.target);
  }
  if ("saver" in aSerializable) {
    download.saver = DownloadSaver.fromSerializable(aSerializable.saver);
  } else {
    download.saver = DownloadSaver.fromSerializable("copy");
  }
  download.saver.download = download;

  if ("startTime" in aSerializable) {
    let time = aSerializable.startTime.getTime
      ? aSerializable.startTime.getTime()
      : aSerializable.startTime;
    download.startTime = new Date(time);
  }

  // If 'errorObj' is present it will take precedence over the 'error' property.
  // 'error' is a legacy property only containing message, which is insufficient
  // to represent all of the error information.
  //
  // Instead of just replacing 'error' we use a new 'errorObj' so that previous
  // versions will keep it as an unknown property.
  if ("errorObj" in aSerializable) {
    download.error = DownloadError.fromSerializable(aSerializable.errorObj);
  } else if ("error" in aSerializable) {
    download.error = aSerializable.error;
  }

  for (let property of kPlainSerializableDownloadProperties) {
    if (property in aSerializable) {
      download[property] = aSerializable[property];
    }
  }

  deserializeUnknownProperties(
    download,
    aSerializable,
    property =>
      !kPlainSerializableDownloadProperties.includes(property) &&
      property != "startTime" &&
      property != "source" &&
      property != "target" &&
      property != "error" &&
      property != "saver"
  );

  return download;
};

/**
 * Represents the source of a download, for example a document or an URI.
 */
export var DownloadSource = function () {};

DownloadSource.prototype = {
  /**
   * String containing the URI for the download source.
   */
  url: null,

  /**
   * String containing the original URL for the download source.
   */
  originalUrl: null,

  /**
   * Indicates whether the download originated from a private window.  This
   * determines the context of the network request that is made to retrieve the
   * resource.
   */
  isPrivate: false,

  /**
   * Represents the referrerInfo of the download source, could be null for
   * example if the download source is not HTTP.
   */
  referrerInfo: null,

  /**
   * For downloads handled by the (default) DownloadCopySaver, this function
   * can adjust the network channel before it is opened, for example to change
   * the HTTP headers or to upload a stream as POST data.
   *
   * @note If this is defined this object will not be serializable, thus the
   *       Download object will not be persisted across sessions.
   *
   * @param aChannel
   *        The nsIChannel to be adjusted.
   *
   * @return {Promise}
   * @resolves When the channel has been adjusted and can be opened.
   * @rejects JavaScript exception that will cause the download to fail.
   */
  adjustChannel: null,

  /**
   * For downloads handled by the (default) DownloadCopySaver, this function
   * will determine, if provided, if a download can progress or has to be
   * cancelled based on the HTTP status code of the network channel.
   *
   * @note If this is defined this object will not be serializable, thus the
   *       Download object will not be persisted across sessions.
   *
   * @param aDownload
   *        The download asking.
   * @param aStatus
   *        The HTTP status in question
   *
   * @return {Boolean} Download can progress
   */
  allowHttpStatus: null,

  /**
   * Represents the loadingPrincipal of the download source,
   * could be null, in which case the system principal is used instead.
   */
  loadingPrincipal: null,

  /**
   * Represents the cookieJarSettings of the download source, could be null if
   * the download source is not from a document.
   */
  cookieJarSettings: null,

  /**
   * Represents the authentication header of the download source, could be null if
   * the download source had no authentication header.
   */
  authHeader: null,
  /**
   * Returns a static representation of the current object state.
   *
   * @return A JavaScript object that can be serialized to JSON.
   */
  toSerializable() {
    if (this.adjustChannel) {
      // If the callback was used, we can't reproduce this across sessions.
      return null;
    }

    if (this.allowHttpStatus) {
      // If the callback was used, we can't reproduce this across sessions.
      return null;
    }

    let serializable = { url: this.url };
    if (this.isPrivate) {
      serializable.isPrivate = true;
    }

    if (this.referrerInfo && isString(this.referrerInfo)) {
      serializable.referrerInfo = this.referrerInfo;
    } else if (this.referrerInfo) {
      serializable.referrerInfo = lazy.E10SUtils.serializeReferrerInfo(
        this.referrerInfo
      );
    }

    if (this.loadingPrincipal) {
      serializable.loadingPrincipal = isString(this.loadingPrincipal)
        ? this.loadingPrincipal
        : lazy.E10SUtils.serializePrincipal(this.loadingPrincipal);
    }

    if (this.cookieJarSettings) {
      serializable.cookieJarSettings = isString(this.cookieJarSettings)
        ? this.cookieJarSettings
        : lazy.E10SUtils.serializeCookieJarSettings(this.cookieJarSettings);
    }

    serializeUnknownProperties(this, serializable);

    // Simplify the representation if we don't have other details.
    if (Object.keys(serializable).length === 1) {
      // serializable's only key is "url", just return the URL as a string.
      return this.url;
    }
    return serializable;
  },
};

/**
 * Creates a new DownloadSource object from its serializable representation.
 *
 * @param aSerializable
 *        Serializable representation of a DownloadSource object.  This may be a
 *        string containing the URI for the download source, an nsIURI, or an
 *        object with the following properties:
 *        {
 *          url: String containing the URI for the download source.
 *          isPrivate: Indicates whether the download originated from a private
 *                     window.  If omitted, the download is public.
 *          referrerInfo: represents the referrerInfo of the download source.
 *                        Can be omitted or null for example if the download
 *                        source is not HTTP.
 *          cookieJarSettings: represents the cookieJarSettings of the download
 *                             source. Can be omitted or null if the download
 *                             source is not from a document.
 *          adjustChannel: For downloads handled by (default) DownloadCopySaver,
 *                         this function can adjust the network channel before
 *                         it is opened, for example to change the HTTP headers
 *                         or to upload a stream as POST data.  Optional.
 *          allowHttpStatus: For downloads handled by the (default)
 *                           DownloadCopySaver, this function will determine, if
 *                           provided, if a download can progress or has to be
 *                           cancelled based on the HTTP status code of the
 *                           network channel.
 *        }
 *
 * @return The newly created DownloadSource object.
 */
DownloadSource.fromSerializable = function (aSerializable) {
  let source = new DownloadSource();
  if (isString(aSerializable)) {
    // Convert String objects to primitive strings at this point.
    source.url = aSerializable.toString();
  } else if (aSerializable instanceof Ci.nsIURI) {
    source.url = aSerializable.spec;
  } else {
    // Convert String objects to primitive strings at this point.
    source.url = aSerializable.url.toString();
    for (let propName of ["isPrivate", "userContextId", "browsingContextId"]) {
      if (propName in aSerializable) {
        source[propName] = aSerializable[propName];
      }
    }
    if ("originalUrl" in aSerializable) {
      source.originalUrl = aSerializable.originalUrl;
    }
    if ("referrerInfo" in aSerializable) {
      // Quick pass, pass directly nsIReferrerInfo, we don't need to serialize
      // and deserialize
      if (aSerializable.referrerInfo instanceof Ci.nsIReferrerInfo) {
        source.referrerInfo = aSerializable.referrerInfo;
      } else {
        source.referrerInfo = lazy.E10SUtils.deserializeReferrerInfo(
          aSerializable.referrerInfo
        );
      }
    }
    if ("loadingPrincipal" in aSerializable) {
      // Quick pass, pass directly nsIPrincipal, we don't need to serialize
      // and deserialize
      if (aSerializable.loadingPrincipal instanceof Ci.nsIPrincipal) {
        source.loadingPrincipal = aSerializable.loadingPrincipal;
      } else {
        source.loadingPrincipal = lazy.E10SUtils.deserializePrincipal(
          aSerializable.loadingPrincipal
        );
      }
    }
    if ("adjustChannel" in aSerializable) {
      source.adjustChannel = aSerializable.adjustChannel;
    }

    if ("allowHttpStatus" in aSerializable) {
      source.allowHttpStatus = aSerializable.allowHttpStatus;
    }

    if ("cookieJarSettings" in aSerializable) {
      if (aSerializable.cookieJarSettings instanceof Ci.nsICookieJarSettings) {
        source.cookieJarSettings = aSerializable.cookieJarSettings;
      } else {
        source.cookieJarSettings = lazy.E10SUtils.deserializeCookieJarSettings(
          aSerializable.cookieJarSettings
        );
      }
    }

    if ("authHeader" in aSerializable) {
      source.authHeader = aSerializable.authHeader;
    }

    deserializeUnknownProperties(
      source,
      aSerializable,
      property =>
        property != "url" &&
        property != "originalUrl" &&
        property != "isPrivate" &&
        property != "referrerInfo" &&
        property != "cookieJarSettings" &&
        property != "authHeader"
    );
  }

  return source;
};

/**
 * Represents the target of a download, for example a file in the global
 * downloads directory, or a file in the system temporary directory.
 */
export var DownloadTarget = function () {};

DownloadTarget.prototype = {
  /**
   * String containing the path of the target file.
   */
  path: null,

  /**
   * String containing the path of the ".part" file containing the data
   * downloaded so far, or null to disable the use of a ".part" file to keep
   * partially downloaded data.
   */
  partFilePath: null,

  /**
   * Indicates whether the target file exists.
   *
   * This is a dynamic property updated when the download finishes or when the
   * "refresh" method of the Download object is called. It can be used by the
   * front-end to reduce I/O compared to checking the target file directly.
   */
  exists: false,

  /**
   * Indicates whether the part file exists. Like `exists`, this is updated
   * dynamically to reduce I/O compared to checking the target file directly.
   */
  partFileExists: false,

  /**
   * Size in bytes of the target file, or zero if the download has not finished.
   *
   * Even if the target file does not exist anymore, this property may still
   * have a value taken from the download metadata. If the metadata has never
   * been available in this session and the size cannot be obtained from the
   * file because it has already been deleted, this property will be zero.
   *
   * For single-file downloads, this property will always match the actual file
   * size on disk, while the totalBytes property of the Download object, when
   * available, may represent the size of the encoded data instead.
   *
   * For downloads involving multiple files, like complete web pages saved to
   * disk, the meaning of this value is undefined. It currently matches the size
   * of the main file only rather than the sum of all the written data.
   *
   * This is a dynamic property updated when the download finishes or when the
   * "refresh" method of the Download object is called. It can be used by the
   * front-end to reduce I/O compared to checking the target file directly.
   */
  size: 0,

  /**
   * Sets the "exists" and "size" properties based on the actual file on disk.
   *
   * @return {Promise}
   * @resolves When the operation has finished successfully.
   * @rejects JavaScript exception.
   */
  async refresh() {
    try {
      this.size = (await IOUtils.stat(this.path)).size;
      this.exists = true;
    } catch (ex) {
      // Report any error not caused by the file not being there. In any case,
      // the size of the download is not updated and the known value is kept.
      if (ex.name != "NotFoundError") {
        console.error(ex);
      }
      this.exists = false;
    }
    this.refreshPartFileState();
  },

  async refreshPartFileState() {
    if (!this.partFilePath) {
      this.partFileExists = false;
      return;
    }
    try {
      this.partFileExists = (await IOUtils.stat(this.partFilePath)).size > 0;
    } catch (ex) {
      if (ex.name != "NotFoundError") {
        console.error(ex);
      }
      this.partFileExists = false;
    }
  },

  /**
   * Returns a static representation of the current object state.
   *
   * @return A JavaScript object that can be serialized to JSON.
   */
  toSerializable() {
    // Simplify the representation if we don't have other details.
    if (!this.partFilePath && !this._unknownProperties) {
      return this.path;
    }

    let serializable = { path: this.path, partFilePath: this.partFilePath };
    serializeUnknownProperties(this, serializable);
    return serializable;
  },
};

/**
 * Creates a new DownloadTarget object from its serializable representation.
 *
 * @param aSerializable
 *        Serializable representation of a DownloadTarget object.  This may be a
 *        string containing the path of the target file, an nsIFile, or an
 *        object with the following properties:
 *        {
 *          path: String containing the path of the target file.
 *          partFilePath: optional string containing the part file path.
 *        }
 *
 * @return The newly created DownloadTarget object.
 */
DownloadTarget.fromSerializable = function (aSerializable) {
  let target = new DownloadTarget();
  if (isString(aSerializable)) {
    // Convert String objects to primitive strings at this point.
    target.path = aSerializable.toString();
  } else if (aSerializable instanceof Ci.nsIFile) {
    // Read the "path" property of nsIFile after checking the object type.
    target.path = aSerializable.path;
  } else {
    // Read the "path" property of the serializable DownloadTarget
    // representation, converting String objects to primitive strings.
    target.path = aSerializable.path.toString();
    if ("partFilePath" in aSerializable) {
      target.partFilePath = aSerializable.partFilePath;
    }

    deserializeUnknownProperties(
      target,
      aSerializable,
      property => property != "path" && property != "partFilePath"
    );
  }
  return target;
};

/**
 * Provides detailed information about a download failure.
 *
 * @param aProperties
 *        Object which may contain any of the following properties:
 *          {
 *            result: Result error code, defaulting to Cr.NS_ERROR_FAILURE
 *            message: String error message to be displayed in the console, or
 *                     null to use the message associated with the result code.
 *            inferCause: If true, attempts to determine if the cause of the
 *                        download is a network failure or a local file failure,
 *                        based on a set of known values of the result code.
 *                        This is useful when the error is received by a
 *                        component that handles both aspects of the download.
 *            localizedReason: If available, is a localized reason for the error
 *                             that can be directly displayed in the UI.
 *          }
 *        The properties object may also contain any of the DownloadError's
 *        because properties, which will be set accordingly in the error object.
 */
export var DownloadError = function (aProperties) {
  const NS_ERROR_MODULE_BASE_OFFSET = 0x45;
  const NS_ERROR_MODULE_NETWORK = 6;
  const NS_ERROR_MODULE_FILES = 13;

  // Set the error name used by the Error object prototype first.
  this.name = "DownloadError";
  this.result = aProperties.result || Cr.NS_ERROR_FAILURE;
  this.localizedReason = aProperties.localizedReason;
  if (aProperties.message) {
--> --------------------

--> maximum size reached

--> --------------------

[ Verzeichnis aufwärts0.43unsichere Verbindung  Übersetzung europäischer Sprachen durch Browser  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge