diff options
Diffstat (limited to 'dom/downloads/DownloadsAPI.jsm')
-rw-r--r-- | dom/downloads/DownloadsAPI.jsm | 365 |
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(); |