mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 22:55:23 +00:00
366 lines
11 KiB
JavaScript
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);
|
|
}
|
|
|
|
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();
|