summaryrefslogtreecommitdiffstats
path: root/dom/downloads/DownloadsAPI.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'dom/downloads/DownloadsAPI.jsm')
-rw-r--r--dom/downloads/DownloadsAPI.jsm365
1 files changed, 365 insertions, 0 deletions
diff --git a/dom/downloads/DownloadsAPI.jsm b/dom/downloads/DownloadsAPI.jsm
new file mode 100644
index 000000000..dfb8286fe
--- /dev/null
+++ b/dom/downloads/DownloadsAPI.jsm
@@ -0,0 +1,365 @@
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Downloads.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageBroadcaster");
+
+/**
+ * Parent process logic that services download API requests from the
+ * DownloadAPI.js instances in content processeses. The actual work of managing
+ * downloads is done by Toolkit's Downloads.jsm. This module is loaded by B2G's
+ * shell.js
+ */
+
+function debug(aStr) {
+#ifdef MOZ_DEBUG
+ dump("-*- DownloadsAPI.jsm : " + aStr + "\n");
+#endif
+}
+
+function sendPromiseMessage(aMm, aMessageName, aData, aError) {
+ debug("sendPromiseMessage " + aMessageName);
+ let msg = {
+ id: aData.id,
+ promiseId: aData.promiseId
+ };
+
+ if (aError) {
+ msg.error = aError;
+ }
+
+ aMm.sendAsyncMessage(aMessageName, msg);
+}
+
+var DownloadsAPI = {
+ init: function() {
+ debug("init");
+
+ this._ids = new WeakMap(); // Maps toolkit download objects to ids.
+ this._index = {}; // Maps ids to downloads.
+
+ ["Downloads:GetList",
+ "Downloads:ClearAllDone",
+ "Downloads:Remove",
+ "Downloads:Pause",
+ "Downloads:Resume",
+ "Downloads:Adopt"].forEach((msgName) => {
+ ppmm.addMessageListener(msgName, this);
+ });
+
+ let self = this;
+ Task.spawn(function () {
+ let list = yield Downloads.getList(Downloads.ALL);
+ yield list.addView(self);
+
+ debug("view added to download list.");
+ }).then(null, Components.utils.reportError);
+
+ this._currentId = 0;
+ },
+
+ /**
+ * Returns a unique id for each download, hashing the url and the path.
+ */
+ downloadId: function(aDownload) {
+ let id = this._ids.get(aDownload, null);
+ if (!id) {
+ id = "download-" + this._currentId++;
+ this._ids.set(aDownload, id);
+ this._index[id] = aDownload;
+ }
+ return id;
+ },
+
+ getDownloadById: function(aId) {
+ return this._index[aId];
+ },
+
+ /**
+ * Converts a download object into a plain json object that we'll
+ * send to the DOM side.
+ */
+ jsonDownload: function(aDownload) {
+ let res = {
+ totalBytes: aDownload.totalBytes,
+ currentBytes: aDownload.currentBytes,
+ url: aDownload.source.url,
+ path: aDownload.target.path,
+ contentType: aDownload.contentType,
+ startTime: aDownload.startTime.getTime(),
+ sourceAppManifestURL: aDownload._unknownProperties &&
+ aDownload._unknownProperties.sourceAppManifestURL
+ };
+
+ if (aDownload.error) {
+ res.error = aDownload.error;
+ }
+
+ res.id = this.downloadId(aDownload);
+
+ // The state of the download. Can be any of "downloading", "stopped",
+ // "succeeded", finalized".
+
+ // Default to "stopped"
+ res.state = "stopped";
+ if (!aDownload.stopped &&
+ !aDownload.canceled &&
+ !aDownload.succeeded &&
+ !aDownload.DownloadError) {
+ res.state = "downloading";
+ } else if (aDownload.succeeded) {
+ res.state = "succeeded";
+ }
+ return res;
+ },
+
+ /**
+ * download view methods.
+ */
+ onDownloadAdded: function(aDownload) {
+ let download = this.jsonDownload(aDownload);
+ debug("onDownloadAdded " + uneval(download));
+ ppmm.broadcastAsyncMessage("Downloads:Added", download);
+ },
+
+ onDownloadRemoved: function(aDownload) {
+ let download = this.jsonDownload(aDownload);
+ download.state = "finalized";
+ debug("onDownloadRemoved " + uneval(download));
+ ppmm.broadcastAsyncMessage("Downloads:Removed", download);
+ this._index[this._ids.get(aDownload)] = null;
+ this._ids.delete(aDownload);
+ },
+
+ onDownloadChanged: function(aDownload) {
+ let download = this.jsonDownload(aDownload);
+ debug("onDownloadChanged " + uneval(download));
+ ppmm.broadcastAsyncMessage("Downloads:Changed", download);
+ },
+
+ receiveMessage: function(aMessage) {
+ if (!aMessage.target.assertPermission("downloads")) {
+ debug("No 'downloads' permission!");
+ return;
+ }
+
+ debug("message: " + aMessage.name);
+
+ switch (aMessage.name) {
+ case "Downloads:GetList":
+ this.getList(aMessage.data, aMessage.target);
+ break;
+ case "Downloads:ClearAllDone":
+ this.clearAllDone(aMessage.data, aMessage.target);
+ break;
+ case "Downloads:Remove":
+ this.remove(aMessage.data, aMessage.target);
+ break;
+ case "Downloads:Pause":
+ this.pause(aMessage.data, aMessage.target);
+ break;
+ case "Downloads:Resume":
+ this.resume(aMessage.data, aMessage.target);
+ break;
+ case "Downloads:Adopt":
+ this.adoptDownload(aMessage.data, aMessage.target);
+ break;
+ default:
+ debug("Invalid message: " + aMessage.name);
+ }
+ },
+
+ getList: function(aData, aMm) {
+ debug("getList called!");
+ let self = this;
+ Task.spawn(function () {
+ let list = yield Downloads.getList(Downloads.ALL);
+ let downloads = yield list.getAll();
+ let res = [];
+ downloads.forEach((aDownload) => {
+ res.push(self.jsonDownload(aDownload));
+ });
+ aMm.sendAsyncMessage("Downloads:GetList:Return", res);
+ }).then(null, Components.utils.reportError);
+ },
+
+ clearAllDone: function(aData, aMm) {
+ debug("clearAllDone called!");
+ Task.spawn(function () {
+ let list = yield Downloads.getList(Downloads.ALL);
+ list.removeFinished();
+ }).then(null, Components.utils.reportError);
+ },
+
+ remove: function(aData, aMm) {
+ debug("remove id " + aData.id);
+ let download = this.getDownloadById(aData.id);
+ if (!download) {
+ sendPromiseMessage(aMm, "Downloads:Remove:Return",
+ aData, "NoSuchDownload");
+ return;
+ }
+
+ Task.spawn(function() {
+ yield download.finalize(true);
+ let list = yield Downloads.getList(Downloads.ALL);
+ yield list.remove(download);
+ }).then(
+ function() {
+ sendPromiseMessage(aMm, "Downloads:Remove:Return", aData);
+ },
+ function() {
+ sendPromiseMessage(aMm, "Downloads:Remove:Return",
+ aData, "RemoveError");
+ }
+ );
+ },
+
+ pause: function(aData, aMm) {
+ debug("pause id " + aData.id);
+ let download = this.getDownloadById(aData.id);
+ if (!download) {
+ sendPromiseMessage(aMm, "Downloads:Pause:Return",
+ aData, "NoSuchDownload");
+ return;
+ }
+
+ download.cancel().then(
+ function() {
+ sendPromiseMessage(aMm, "Downloads:Pause:Return", aData);
+ },
+ function() {
+ sendPromiseMessage(aMm, "Downloads:Pause:Return",
+ aData, "PauseError");
+ }
+ );
+ },
+
+ resume: function(aData, aMm) {
+ debug("resume id " + aData.id);
+ let download = this.getDownloadById(aData.id);
+ if (!download) {
+ sendPromiseMessage(aMm, "Downloads:Resume:Return",
+ aData, "NoSuchDownload");
+ return;
+ }
+
+ download.start().then(
+ function() {
+ sendPromiseMessage(aMm, "Downloads:Resume:Return", aData);
+ },
+ function() {
+ sendPromiseMessage(aMm, "Downloads:Resume:Return",
+ aData, "ResumeError");
+ }
+ );
+ },
+
+ /**
+ * Receive a download to adopt in the same representation we produce from
+ * our "jsonDownload" normalizer and add it to the list of downloads.
+ */
+ adoptDownload: function(aData, aMm) {
+ let adoptJsonRep = aData.jsonDownload;
+ debug("adoptDownload " + uneval(adoptJsonRep));
+
+ Task.spawn(function* () {
+ // Verify that the file exists on disk. This will result in a rejection
+ // if the file does not exist. We will also use this information for the
+ // file size to avoid weird inconsistencies. We ignore the filesystem
+ // timestamp in favor of whatever the caller is telling us.
+ let fileInfo = yield OS.File.stat(adoptJsonRep.path);
+
+ // We also require that the file is not a directory.
+ if (fileInfo.isDir) {
+ throw new Error("AdoptFileIsDirectory");
+ }
+
+ // We need to create a Download instance to add to the list. Create a
+ // serialized representation and then from there the instance.
+ let serializedRep = {
+ // explicit initializations in toSerializable
+ source: {
+ url: adoptJsonRep.url
+ // This is where isPrivate would go if adoption supported private
+ // browsing.
+ },
+ target: {
+ path: adoptJsonRep.path,
+ },
+ startTime: adoptJsonRep.startTime,
+ // kPlainSerializableDownloadProperties propagations
+ succeeded: true, // (all adopted downloads are required to be completed)
+ totalBytes: fileInfo.size,
+ contentType: adoptJsonRep.contentType,
+ // unknown properties added/used by the DownloadsAPI
+ currentBytes: fileInfo.size,
+ sourceAppManifestURL: adoptJsonRep.sourceAppManifestURL
+ };
+
+ let download = yield Downloads.createDownload(serializedRep);
+
+ // The ALL list is a DownloadCombinedList instance that combines the
+ // PUBLIC (persisted to disk) and PRIVATE (ephemeral) download lists..
+ // When we call add on it, it dispatches to the appropriate list based on
+ // the 'isPrivate' field of the source. (Which we don't initialize and
+ // defaults to false.)
+ let allDownloadList = yield Downloads.getList(Downloads.ALL);
+
+ // This add will automatically notify all views of the added download,
+ // including DownloadsAPI instances and the DownloadAutoSaveView that's
+ // subscribed to the PUBLIC list and will save the download.
+ yield allDownloadList.add(download);
+
+ debug("download adopted");
+ // The notification above occurred synchronously, and so we will have
+ // already dispatched an added notification for our download to the child
+ // process in question. As such, we only need to relay the download id
+ // since the download will already have been cached.
+ return download;
+ }.bind(this)).then(
+ (download) => {
+ sendPromiseMessage(aMm, "Downloads:Adopt:Return",
+ {
+ id: this.downloadId(download),
+ promiseId: aData.promiseId
+ });
+ },
+ (ex) => {
+ let reportAs = "AdoptError";
+ // Provide better error codes for expected errors.
+ if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ reportAs = "AdoptNoSuchFile";
+ } else if (ex.message === "AdoptFileIsDirectory") {
+ reportAs = ex.message;
+ } else {
+ // Anything else is unexpected and should be reported to help track
+ // down what's going wrong.
+ debug("unexpected download error: " + ex);
+ Cu.reportError(ex);
+ }
+ sendPromiseMessage(aMm, "Downloads:Adopt:Return",
+ {
+ promiseId: aData.promiseId
+ },
+ reportAs);
+ });
+ }
+};
+
+DownloadsAPI.init();