--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -29,16 +29,18 @@ var {
const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
"danger", "mime", "startTime", "endTime",
"estimatedEndTime", "state",
"paused", "canResume", "error",
"bytesReceived", "totalBytes",
"fileSize", "exists",
"byExtensionId", "byExtensionName"];
+const DOWNLOAD_DATE_FIELDS = ["startTime", "endTime", "estimatedEndTime"];
+
// Fields that we generate onChanged events for.
const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume",
"error", "exists"];
// From https://fetch.spec.whatwg.org/#forbidden-header-name
const FORBIDDEN_HEADERS = ["ACCEPT-CHARSET", "ACCEPT-ENCODING",
"ACCESS-CONTROL-REQUEST-HEADERS", "ACCESS-CONTROL-REQUEST-METHOD",
"CONNECTION", "CONTENT-LENGTH", "COOKIE", "COOKIE2", "DATE", "DNT",
@@ -57,18 +59,31 @@ class DownloadItem {
get url() { return this.download.source.url; }
get referrer() { return this.download.source.referrer; }
get filename() { return this.download.target.path; }
get incognito() { return this.download.source.isPrivate; }
get danger() { return "safe"; } // TODO
get mime() { return this.download.contentType; }
get startTime() { return this.download.startTime; }
- get endTime() { return null; } // TODO
- get estimatedEndTime() { return null; } // TODO
+ get endTime() {
+ if (this.download.endTime &&
+ (this.download.succeeded || this.download.canceled ||
+ this.download.stopped || this.download.error)) {
+ return new Date(this.download.endTime);
+ }
+ }
+ get estimatedEndTime() {
+ // Based on the code in summarizeDownloads() in DownloadsCommon.jsm
+ if (this.download.hasProgress && this.download.speed > 0) {
+ let sizeLeft = this.download.totalBytes - this.download.currentBytes;
+ let rawTimeLeft = sizeLeft / this.download.speed;
+ return new Date(Date.now() + rawTimeLeft);
+ }
+ }
get state() {
if (this.download.succeeded) {
return "complete";
}
if (this.download.canceled) {
return "interrupted";
}
return "in_progress";
@@ -118,18 +133,20 @@ class DownloadItem {
* @returns {object} A DownloadItem with flat properties,
* suitable for cloning.
*/
serialize() {
let obj = {};
for (let field of DOWNLOAD_ITEM_FIELDS) {
obj[field] = this[field];
}
- if (obj.startTime) {
- obj.startTime = obj.startTime.toISOString();
+ for (let field of DOWNLOAD_DATE_FIELDS) {
+ if (obj[field]) {
+ obj[field] = obj[field].toISOString();
+ }
}
return obj;
}
// When a change event fires, handlers can look at how an individual
// field changed by comparing item.fieldname with item.prechange.fieldname.
// After all handlers have been invoked, this gets called to store the
// current values of all fields ahead of the next event.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
@@ -43,16 +43,23 @@ function handleRequest(request, response
response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false);
response.finish();
return;
}
response.setStatusLine(request.httpVersion, 206, "Partial Content");
response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false);
response.write(TEST_DATA.slice(start, end + 1));
+ } else if (request.queryString.includes("stream")) {
+ response.processAsync();
+ response.setHeader("Content-Length", "10000", false);
+ response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ setInterval(() => {
+ response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ }, 50);
} else {
response.processAsync();
response.setHeader("Content-Length", `${TOTAL_LEN}`, false);
response.write(TEST_DATA.slice(0, PARTIAL_LEN));
}
do_register_cleanup(() => {
try {
@@ -212,25 +219,25 @@ function runInExtension(what, ...args) {
extension.sendMessage(`${what}.request`, ...args);
return extension.awaitMessage(`${what}.done`);
}
// This is pretty simplistic, it looks for a progress update for a
// download of the given url in which the total bytes are exactly equal
// to the given value. Unless you know exactly how data will arrive from
// the server (eg see interruptible.sjs), it probably isn't very useful.
-async function waitForProgress(url, bytes) {
+async function waitForProgress(url, testFn) {
let list = await Downloads.getList(Downloads.ALL);
return new Promise(resolve => {
const view = {
onDownloadChanged(download) {
- if (download.source.url == url && download.currentBytes == bytes) {
+ if (download.source.url == url && testFn(download.currentBytes)) {
list.removeView(view);
- resolve();
+ resolve(download.currentBytes);
}
},
};
list.addView(view);
});
}
add_task(async function setup() {
@@ -288,17 +295,17 @@ add_task(async function test_events() {
add_task(async function test_cancel() {
let url = getInterruptibleUrl();
do_print(url);
let msg = await runInExtension("download", {url});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
- let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+ let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
msg = await runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
equal(msg.status, "success", "got created and changed events");
await progressPromise;
do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -343,16 +350,18 @@ add_task(async function test_cancel() {
equal(msg.status, "success", "got onChanged events corresponding to cancel()");
msg = await runInExtension("search", {error: "USER_CANCELED"});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].id, id, "download.id is correct");
equal(msg.result[0].state, "interrupted", "download.state is correct");
equal(msg.result[0].paused, false, "download.paused is correct");
+ ok(Date.parse(msg.result[0].endTime) != NaN, "download.endTime is correct");
+ equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
equal(msg.result[0].canResume, false, "download.canResume is correct");
equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, false, "download.exists is correct");
msg = await runInExtension("pause", id);
equal(msg.status, "error", "cannot pause a canceled download");
@@ -361,17 +370,17 @@ add_task(async function test_cancel() {
});
add_task(async function test_pauseresume() {
let url = getInterruptibleUrl();
let msg = await runInExtension("download", {url});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
- let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+ let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
msg = await runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
equal(msg.status, "success", "got created and changed events");
await progressPromise;
do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -410,16 +419,18 @@ add_task(async function test_pauseresume
equal(msg.status, "success", "got onChanged event corresponding to pause");
msg = await runInExtension("search", {paused: true});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].id, id, "download.id is correct");
equal(msg.result[0].state, "interrupted", "download.state is correct");
equal(msg.result[0].paused, true, "download.paused is correct");
+ ok(Date.parse(msg.result[0].endTime) != NaN, "download.endTime is correct");
+ equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
equal(msg.result[0].canResume, true, "download.canResume is correct");
equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, false, "download.exists is correct");
msg = await runInExtension("search", {error: "USER_CANCELED"});
equal(msg.status, "success", "search() succeeded");
@@ -468,16 +479,18 @@ add_task(async function test_pauseresume
]);
equal(msg.status, "success", "got onChanged events for resume and complete");
msg = await runInExtension("search", {id});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].state, "complete", "download.state is correct");
equal(msg.result[0].paused, false, "download.paused is correct");
+ ok(Date.parse(msg.result[0].endTime) != NaN, "download.endTime is correct");
+ equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
equal(msg.result[0].canResume, false, "download.canResume is correct");
equal(msg.result[0].error, null, "download.error is correct");
equal(msg.result[0].bytesReceived, INT_TOTAL_LEN, "download.bytesReceived is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, true, "download.exists is correct");
msg = await runInExtension("pause", id);
equal(msg.status, "error", "cannot pause a completed download");
@@ -487,17 +500,17 @@ add_task(async function test_pauseresume
});
add_task(async function test_pausecancel() {
let url = getInterruptibleUrl();
let msg = await runInExtension("download", {url});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
- let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+ let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
msg = await runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
equal(msg.status, "success", "got created and changed events");
await progressPromise;
do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -536,16 +549,18 @@ add_task(async function test_pausecancel
equal(msg.status, "success", "got onChanged event corresponding to pause");
msg = await runInExtension("search", {paused: true});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].id, id, "download.id is correct");
equal(msg.result[0].state, "interrupted", "download.state is correct");
equal(msg.result[0].paused, true, "download.paused is correct");
+ ok(Date.parse(msg.result[0].endTime) != NaN, "download.endTime is correct");
+ equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
equal(msg.result[0].canResume, true, "download.canResume is correct");
equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, false, "download.exists is correct");
msg = await runInExtension("search", {error: "USER_CANCELED"});
equal(msg.status, "success", "search() succeeded");
@@ -573,16 +588,18 @@ add_task(async function test_pausecancel
]);
equal(msg.status, "success", "got onChanged event for cancel");
msg = await runInExtension("search", {id});
equal(msg.status, "success", "search() succeeded");
equal(msg.result.length, 1, "search() found 1 download");
equal(msg.result[0].state, "interrupted", "download.state is correct");
equal(msg.result[0].paused, false, "download.paused is correct");
+ ok(Date.parse(msg.result[0].endTime) != NaN, "download.endTime is correct");
+ equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
equal(msg.result[0].canResume, false, "download.canResume is correct");
equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
equal(msg.result[0].exists, false, "download.exists is correct");
});
add_task(async function test_pause_resume_cancel_badargs() {
let BAD_ID = 1000;
@@ -633,17 +650,17 @@ add_task(async function test_file_remova
});
add_task(async function test_removal_of_incomplete_download() {
let url = getInterruptibleUrl();
let msg = await runInExtension("download", {url});
equal(msg.status, "success", "download() succeeded");
const id = msg.result;
- let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+ let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
msg = await runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
equal(msg.status, "success", "got created and changed events");
await progressPromise;
do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -852,11 +869,36 @@ add_task(async function test_getFileIcon
msg = await runInExtension("getFileIcon", id, {size: 128});
equal(msg.status, "error", "getFileIcon() fails");
ok(msg.errmsg.includes("Error processing size"), "size is too big");
webNav.close();
});
+add_task(async function test_startendtimes() {
+ let url = `${getInterruptibleUrl()}&stream=1`;
+ let msg = await runInExtension("download", {url});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let previousBytes = await waitForProgress(url, bytes => bytes > 0);
+ await waitForProgress(url, bytes => bytes > previousBytes);
+
+ msg = await runInExtension("search", {id});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].endTime, null, "download.endTime is correct");
+ ok(msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct");
+ ok(msg.result[0].bytesReceived > 0, "download.bytesReceived is correct");
+
+ msg = await runInExtension("cancel", id);
+
+ msg = await runInExtension("search", {id});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ ok(msg.result[0].endTime, "download.endTime is correct");
+ ok(!msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct");
+});
+
add_task(async function cleanup() {
await extension.unload();
});