Bug 1256269 - Support endTime and estimatedEndTime in DownloadItem; r?aswan, paolo draft
authorThomas Wisniewski <wisniewskit@gmail.com>
Sun, 16 Jul 2017 00:05:24 -0400
changeset 610056 e08d7d796c80bc7cf14ba98ed48563d34fb9a9c2
parent 609403 13a2e506992ccf07c1358d9f22cbf2dfdcb0120f
child 637754 1e56a57ccb1f601d284c0aa7df3570a39179f528
push id68775
push userwisniewskit@gmail.com
push dateMon, 17 Jul 2017 21:04:08 +0000
reviewersaswan, paolo
bugs1256269
milestone56.0a1
Bug 1256269 - Support endTime and estimatedEndTime in DownloadItem; r?aswan, paolo paolo, could you please review the changes to DownloadCore.jsm? MozReview-Commit-ID: Ed2PhuNmw1y
toolkit/components/extensions/ext-downloads.js
toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
toolkit/components/jsdownloads/src/DownloadCore.jsm
--- 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();
 });
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -200,16 +200,23 @@ this.Download.prototype = {
   /**
    * Indicates the start time of the download.  When the download starts,
    * this property is set to a valid Date object.  The default value is null
    * before the download starts.
    */
   startTime: null,
 
   /**
+   * Indicates the end time of the download.  When the download ends,
+   * this property is set to a valid Date object.  The default value is null
+   * before the download ends.
+   */
+  endTime: null,
+
+  /**
    * Indicates whether this download's "progress" property is able to report
    * partial progress while the download proceeds, and whether the value in
    * totalBytes is relevant.  This depends on the saver and the download source.
    */
   hasProgress: false,
 
   /**
    * Progress percent, from 0 to 100.  Intermediate values are reported only if
@@ -494,16 +501,17 @@ this.Download.prototype = {
 
           // Cancellation exceptions will be changed in the catch block below.
           throw new DownloadError();
         }
 
         // Update the status properties for a successful download.
         this.progress = 100;
         this.succeeded = true;
+        this.endTime = new Date();
         this.hasPartialData = false;
       } catch (originalEx) {
         // We may choose a different exception to propagate in the code below,
         // or wrap the original one. We do this mutation in a different variable
         // because of the "no-ex-assign" ESLint rule.
         let ex = originalEx;
 
         // Fail with a generic status code on cancellation, so that the caller
@@ -542,16 +550,17 @@ this.Download.prototype = {
         // Any cancellation request has now been processed.
         this._saverExecuting = false;
         this._promiseCanceled = null;
 
         // Update the status properties, unless a new attempt already started.
         if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
           this._currentAttempt = null;
           this.stopped = true;
+          this.endTime = new Date();
           this.speed = 0;
           this._notifyChange();
           if (this.succeeded) {
             await this._succeed();
           }
         }
       }
     })());
@@ -635,16 +644,17 @@ this.Download.prototype = {
         await this.target.refresh();
       } catch (ex) {
         await this.refresh();
         this._promiseUnblock = null;
         throw ex;
       }
 
       this.succeeded = true;
+      this.endTime = new Date();
       this.hasBlockedData = false;
       this._notifyChange();
       await this._succeed();
     })();
 
     return this._promiseUnblock;
   },
 
@@ -777,16 +787,17 @@ this.Download.prototype = {
         this._currentAttempt.then(resolve, resolve);
       });
 
       // The download can already be restarted.
       this._currentAttempt = null;
 
       // Notify that the cancellation request was received.
       this.canceled = true;
+      this.endTime = new Date();
       this._notifyChange();
 
       // Execute the actual cancellation through the saver object, in case it
       // has already started.  Otherwise, the cancellation will be handled just
       // before the saver is started.
       if (this._saverExecuting) {
         this.saver.cancel();
       }