Bug 1245597 - implement the basics of chrome.downloads.download() r=kmag

This commit is contained in:
Andrew Swan 2016-02-21 21:20:22 -08:00
parent cfd73d0d46
commit 058b731776
6 changed files with 326 additions and 3 deletions

View File

@ -2,14 +2,104 @@
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
ignoreEvent,
} = ExtensionUtils;
let currentId = 0;
extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
return {
downloads: {
download(options) {
if (options.filename != null) {
if (options.filename.length == 0) {
return Promise.reject({message: "filename must not be empty"});
}
let path = OS.Path.split(options.filename);
if (path.absolute) {
return Promise.reject({message: "filename must not be an absolute path"});
}
if (path.components.some(component => component == "..")) {
return Promise.reject({message: "filename must not contain back-references (..)"});
}
}
if (options.conflictAction == "prompt") {
// TODO
return Promise.reject({message: "conflictAction prompt not yet implemented"});
}
function createTarget(downloadsDir) {
// TODO
// if (options.saveAs) { }
let target;
if (options.filename) {
target = OS.Path.join(downloadsDir, options.filename);
} else {
let uri = NetUtil.newURI(options.url).QueryInterface(Ci.nsIURL);
target = OS.Path.join(downloadsDir, uri.fileName);
}
// This has a race, something else could come along and create
// the file between this test and them time the download code
// creates the target file. But we can't easily fix it without
// modifying DownloadCore so we live with it for now.
return OS.File.exists(target).then(exists => {
if (exists) {
switch (options.conflictAction) {
case "uniquify":
default:
target = DownloadPaths.createNiceUniqueFile(new FileUtils.File(target)).path;
break;
case "overwrite":
break;
}
}
return target;
});
}
let download;
return Downloads.getPreferredDownloadsDirectory()
.then(downloadsDir => createTarget(downloadsDir))
.then(target => Downloads.createDownload({
source: options.url,
target: target,
})).then(dl => {
download = dl;
return Downloads.getList(Downloads.ALL);
}).then(list => {
list.add(download);
// This is necessary to make pause/resume work.
download.tryToKeepPartialData = true;
download.start();
// Without other chrome.downloads methods, we can't actually
// do anything with the id so just return a dummy value for now.
return currentId++;
});
},
// When we do open(), check for additional downloads.open permission.
// i.e.:
// open(downloadId) {

View File

@ -19,4 +19,5 @@ DIRS += ['schemas']
JAR_MANIFESTS += ['jar.mn']
MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']

View File

@ -22,7 +22,7 @@
"id": "FilenameConflictAction",
"type": "string",
"enum": [
"uniqify",
"uniquify",
"overwrite",
"prompt"
]
@ -214,7 +214,7 @@
{
"name": "download",
"type": "function",
"unsupported": true,
"async": "callback",
"description": "Download a URL. If the URL uses the HTTP[S] protocol, then the request will include all cookies currently set for its hostname. If both <code>filename</code> and <code>saveAs</code> are specified, then the Save As dialog will be displayed, pre-populated with the specified <code>filename</code>. If the download started successfully, <code>callback</code> will be called with the new <a href='#type-DownloadItem'>DownloadItem</a>'s <code>downloadId</code>. If there was an error starting the download, then <code>callback</code> will be called with <code>downloadId=undefined</code> and <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain a descriptive string. The error strings are not guaranteed to remain backwards compatible between releases. You must not parse it.",
"parameters": [
{
@ -224,7 +224,8 @@
"properties": {
"url": {
"description": "The URL to download.",
"type": "string"
"type": "string",
"format": "url"
},
"filename": {
"description": "A file path relative to the Downloads directory to contain the downloaded file.",
@ -236,11 +237,13 @@
"optional": true
},
"saveAs": {
"unsupported": true,
"description": "Use a file-chooser to allow the user to select a filename.",
"optional": true,
"type": "boolean"
},
"method": {
"unsupported": true,
"description": "The HTTP method to use if the URL uses the HTTP[S] protocol.",
"enum": [
"GET",
@ -250,6 +253,7 @@
"type": "string"
},
"headers": {
"unsupported": true,
"optional": true,
"type": "array",
"description": "Extra HTTP headers to send with the request if the URL uses the HTTP[s] protocol. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>, restricted to those allowed by XMLHttpRequest.",
@ -268,6 +272,7 @@
}
},
"body": {
"unsupported": true,
"description": "Post body.",
"optional": true,
"type": "string"

View File

@ -0,0 +1,5 @@
[DEFAULT]
support-files =
file_download.txt
[test_chrome_ext_downloads_download.html]

View File

@ -0,0 +1 @@
This is a sample file used in download tests.

View File

@ -0,0 +1,221 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebExtension test</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="text/javascript">
"use strict";
const {
interfaces: Ci,
utils: Cu,
} = Components;
/* global OS */
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Downloads.jsm");
Cu.import("resource://gre/modules/Services.jsm");
const WINDOWS = (AppConstants.platform == "win");
const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
const FILE_NAME = "file_download.txt";
const FILE_URL = BASE + "/" + FILE_NAME;
const FILE_NAME_UNIQUE = "file_download(1).txt";
const FILE_LEN = 46;
let downloadDir;
function setup() {
downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
info(`Using download directory ${downloadDir.path}`);
Services.prefs.setIntPref("browser.download.folderList", 2);
Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir);
SimpleTest.registerCleanupFunction(() => {
Services.prefs.clearUserPref("browser.download.folderList");
Services.prefs.clearUserPref("browser.download.dir");
});
}
function backgroundScript() {
browser.test.onMessage.addListener(function(msg) {
if (msg == "download.request") {
// download() throws on bad arguments, we can remove the extra
// promise when bug 1250223 is fixed.
return Promise.resolve().then(() => browser.downloads.download(arguments[1]))
.then((id) => browser.test.sendMessage("download.done", {status: "success", id}))
.catch(error => browser.test.sendMessage("download.done", {status: "error", errmsg: error.message}));
}
});
browser.test.sendMessage("ready");
}
// This function is a bit of a sledgehammer, it looks at every download
// the browser knows about and waits for all active downloads to complete.
// But we only start one at a time and only do a handful in total, so
// this lets us test download() without depending on anything else.
function waitForDownloads() {
return Downloads.getList(Downloads.ALL)
.then(list => list.getAll())
.then(downloads => {
let inprogress = downloads.filter(dl => !dl.stopped);
return Promise.all(inprogress.map(dl => dl.whenSucceeded()));
});
}
// Create a file in the downloads directory.
function touch(filename) {
let file = downloadDir.clone();
file.append(filename);
file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
}
// Remove a file in the downloads directory.
function remove(filename) {
let file = downloadDir.clone();
file.append(filename);
file.remove(false);
}
add_task(function* test_downloads() {
setup();
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {
permissions: ["downloads"],
},
});
function download(options) {
extension.sendMessage("download.request", options);
return extension.awaitMessage("download.done");
}
function testDownload(options, localFile, expectedSize, description) {
return download(options).then(msg => {
is(msg.status, "success", `downloads.download() works with ${description}`);
return waitForDownloads();
}).then(() => {
let localPath = downloadDir.clone();
localPath.append(localFile);
is(localPath.fileSize, expectedSize, "Downloaded file has expected size");
localPath.remove(false);
});
}
yield extension.startup();
yield extension.awaitMessage("ready");
info("extension started");
// Call download() with just the url property.
yield testDownload({url: FILE_URL}, FILE_NAME, FILE_LEN, "just source");
// Call download() with a filename property.
yield testDownload({
url: FILE_URL,
filename: "newpath.txt",
}, "newpath.txt", FILE_LEN, "source and filename");
// Check conflictAction of "uniquify".
touch(FILE_NAME);
yield testDownload({
url: FILE_URL,
conflictAction: "uniquify",
}, FILE_NAME_UNIQUE, FILE_LEN, "conflictAction=uniquify");
// todo check that preexisting file was not modified?
remove(FILE_NAME);
// Check conflictAction of "overwrite".
touch(FILE_NAME);
yield testDownload({
url: FILE_URL,
conflictAction: "overwrite",
}, FILE_NAME, FILE_LEN, "conflictAction=overwrite");
// Try to download in invalid url
yield download({url: "this is not a valid URL"}).then(msg => {
is(msg.status, "error", "downloads.download() fails with invalid url");
ok(/not a valid URL/.test(msg.errmsg), "error message for invalid url is correct");
});
// Try to download to an empty path.
yield download({
url: FILE_URL,
filename: "",
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with empty filename");
is(msg.errmsg, "filename must not be empty", "error message for empty filename is correct");
});
// Try to download to an absolute path.
const absolutePath = OS.Path.join(WINDOWS ? "\\tmp" : "/tmp", "file_download.txt");
yield download({
url: FILE_URL,
filename: absolutePath,
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with absolute filename");
is(msg.errmsg, "filename must not be an absolute path", `error message for absolute path (${absolutePath}) is correct`);
});
if (WINDOWS) {
yield download({
url: FILE_URL,
filename: "C:\\file_download.txt",
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with absolute filename");
is(msg.errmsg, "filename must not be an absolute path", "error message for absolute path with drive letter is correct");
});
}
// Try to download to a relative path containing ..
yield download({
url: FILE_URL,
filename: OS.Path.join("..", "file_download.txt"),
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with back-references");
is(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
});
// Try to download to a long relative path containing ..
yield download({
url: FILE_URL,
filename: OS.Path.join("foo", "..", "..", "file_download.txt"),
}).then(msg => {
is(msg.status, "error", "downloads.download() fails with back-references");
is(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
});
yield extension.unload();
});
// check for leftover files in the download directory
add_task(function*() {
let entries = downloadDir.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
ok(false, `Leftover file ${entry.path} in download directory`);
entry.remove(false);
}
downloadDir.remove(false);
});
</script>
</body>
</html>