gecko/dom/downloads/DownloadsAPI.jsm
Andrew Sutherland b8c2f78875 Bug 825318 - Implement adoptDownload for mozDownloadManager, r=aus, r=sicking
Implement mozDownloadManager.adoptDownload as a certified-only API.

This also fixes and re-enables many of the existing dom/downloads tests
failures by virtue of cleanup and not running them on non-gonk toolkits
where exceptions will be thrown and things will fail.  This should
resolve bug 979446 about re-enabling the tests.
2015-02-24 11:06:59 -05:00

366 lines
11 KiB
JavaScript

/* 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);
}
let 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();