Bug 835875 - Add the ability to cancel downloads. r=enn

This commit is contained in:
Paolo Amadini 2013-02-19 10:10:50 +01:00
parent 19128f1970
commit 0ebc3e70a9
3 changed files with 201 additions and 69 deletions

View File

@ -71,7 +71,7 @@ const BackgroundFileSaverStreamListener = Components.Constructor(
*/
function Download()
{
this._deferDone = Promise.defer();
this._deferStopped = Promise.defer();
}
Download.prototype = {
@ -93,10 +93,15 @@ Download.prototype = {
/**
* Becomes true when the download has been completed successfully, failed, or
* has been canceled. This property can become true, then it can be reset to
* false when a failed or canceled download is resumed. This property remains
* false while the download is paused.
* false when a failed or canceled download is restarted.
*/
done: false,
stopped: false,
/**
* Indicates that the download has been canceled. This property can become
* true, then it can be reset to false when a canceled download is restarted.
*/
canceled: false,
/**
* When the download fails, this is set to a DownloadError instance indicating
@ -141,7 +146,7 @@ Download.prototype = {
currentBytes: 0,
/**
* This can be set to a function that is called when other properties change.
* This can be set to a function that is called after other properties change.
*/
onchange: null,
@ -162,7 +167,7 @@ Download.prototype = {
* This deferred object is resolved when this download finishes successfully,
* and rejected if this download fails.
*/
_deferDone: null,
_deferStopped: null,
/**
* Starts the download.
@ -173,31 +178,36 @@ Download.prototype = {
*/
start: function D_start()
{
this._deferDone.resolve(Task.spawn(function task_D_start() {
this._deferStopped.resolve(Task.spawn(function task_D_start() {
try {
yield this.saver.execute();
this.progress = 100;
} catch (ex) {
if (this.canceled) {
throw new DownloadError(Cr.NS_ERROR_FAILURE, "Download canceled.");
}
this.error = ex;
throw ex;
} finally {
this.done = true;
this.stopped = true;
this._notifyChange();
}
}.bind(this)));
return this.whenDone();
return this._deferStopped.promise;
},
/**
* Waits for the download to finish.
*
* @return {Promise}
* @resolves When the download has finished successfully.
* @rejects JavaScript exception if the download failed.
* Cancels the download.
*/
whenDone: function D_whenDone() {
return this._deferDone.promise;
cancel: function D_cancel()
{
if (this.stopped || this.canceled) {
return;
}
this.canceled = true;
this.saver.cancel();
},
/**
@ -334,7 +344,15 @@ DownloadSaver.prototype = {
execute: function DS_execute()
{
throw new Error("Not implemented.");
}
},
/**
* Cancels the download.
*/
cancel: function DS_cancel()
{
throw new Error("Not implemented.");
},
};
////////////////////////////////////////////////////////////////////////////////
@ -348,6 +366,11 @@ function DownloadCopySaver() { }
DownloadCopySaver.prototype = {
__proto__: DownloadSaver.prototype,
/**
* BackgroundFileSaver object currently handling the download.
*/
_backgroundFileSaver: null,
/**
* Implements "DownloadSaver.execute".
*/
@ -424,6 +447,9 @@ DownloadCopySaver.prototype = {
aOffset, aCount);
},
}, null);
// If the operation succeeded, store the object to allow cancellation.
this._backgroundFileSaver = backgroundFileSaver;
} catch (ex) {
// In case an error occurs while setting up the chain of objects for the
// download, ensure that we release the resources of the background saver.
@ -432,4 +458,15 @@ DownloadCopySaver.prototype = {
}
return deferred.promise;
},
/**
* Implements "DownloadSaver.cancel".
*/
cancel: function DCS_cancel()
{
if (this._backgroundFileSaver) {
this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
this._backgroundFileSaver = null;
}
},
};

View File

@ -48,6 +48,10 @@ const FAKE_BASE = "http://localhost:" + FAKE_SERVER_PORT;
const TEST_SOURCE_URI = NetUtil.newURI(HTTP_BASE + "/source.txt");
const TEST_FAKE_SOURCE_URI = NetUtil.newURI(FAKE_BASE + "/source.txt");
const TEST_INTERRUPTIBLE_PATH = "/interruptible.txt";
const TEST_INTERRUPTIBLE_URI = NetUtil.newURI(HTTP_BASE +
TEST_INTERRUPTIBLE_PATH);
const TEST_TARGET_FILE_NAME = "test-download.txt";
const TEST_DATA_SHORT = "This test string is downloaded.";
@ -135,6 +139,48 @@ function startFakeServer()
return serverSocket;
}
/**
* This function allows testing events or actions that need to happen in the
* middle of a download.
*
* Calling this function registers a new request handler in the internal HTTP
* server, accessible at the TEST_INTERRUPTIBLE_URI address. The HTTP handler
* returns the TEST_DATA_SHORT text, then waits until the "resolve" method is
* called on the object returned by the function. At this point, the handler
* sends the TEST_DATA_SHORT text again to complete the response.
*
* You can also call the "reject" method on the returned object to interrupt the
* response midway. Because of how the network layer is implemented, this does
* not cause the socket to return an error.
*
* The handler is unregistered when the response finishes or is interrupted.
*
* @returns Deferred object used to control the response.
*/
function startInterruptibleResponseHandler()
{
let deferResponse = Promise.defer();
gHttpServer.registerPathHandler(TEST_INTERRUPTIBLE_PATH,
function (aRequest, aResponse)
{
aResponse.processAsync();
aResponse.setHeader("Content-Type", "text/plain", false);
aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
false);
aResponse.write(TEST_DATA_SHORT);
deferResponse.promise.then(function SIRH_onSuccess() {
aResponse.write(TEST_DATA_SHORT);
aResponse.finish();
gHttpServer.registerPathHandler(TEST_INTERRUPTIBLE_PATH, null);
}, function SIRH_onFailure() {
aResponse.abort();
gHttpServer.registerPathHandler(TEST_INTERRUPTIBLE_PATH, null);
});
});
return deferResponse;
}
////////////////////////////////////////////////////////////////////////////////
//// Initialization functions common to all tests

View File

@ -12,9 +12,20 @@
////////////////////////////////////////////////////////////////////////////////
//// Globals
function promiseSimpleDownload() {
/**
* Creates a new Download object, using TEST_TARGET_FILE_NAME as the target.
* The target is deleted by getTempFile when this function is called.
*
* @param aSourceURI
* The nsIURI for the download source, or null to use TEST_SOURCE_URI.
*
* @return {Promise}
* @resolves The newly created Download object.
* @rejects JavaScript exception.
*/
function promiseSimpleDownload(aSourceURI) {
return Downloads.createDownload({
source: { uri: TEST_SOURCE_URI },
source: { uri: aSourceURI || TEST_SOURCE_URI },
target: { file: getTempFile(TEST_TARGET_FILE_NAME) },
saver: { type: "copy" },
});
@ -36,6 +47,10 @@ add_task(function test_download_construction()
saver: { type: "copy" },
});
// Checks the generated DownloadSource and DownloadTarget properties.
do_check_true(download.source.uri.equals(TEST_SOURCE_URI));
do_check_eq(download.target.file, targetFile);
// Starts the download and waits for completion.
yield download.start();
@ -49,13 +64,13 @@ add_task(function test_download_initial_final_state()
{
let download = yield promiseSimpleDownload();
do_check_false(download.done);
do_check_false(download.stopped);
do_check_eq(download.progress, 0);
// Starts the download and waits for completion.
yield download.start();
do_check_true(download.done);
do_check_true(download.stopped);
do_check_eq(download.progress, 100);
});
@ -67,11 +82,11 @@ add_task(function test_download_view_final_notified()
let download = yield promiseSimpleDownload();
let onchangeNotified = false;
let lastNotifiedDone;
let lastNotifiedStopped;
let lastNotifiedProgress;
download.onchange = function () {
onchangeNotified = true;
lastNotifiedDone = download.done;
lastNotifiedStopped = download.stopped;
lastNotifiedProgress = download.progress;
};
@ -80,7 +95,7 @@ add_task(function test_download_view_final_notified()
// The view should have been notified before the download completes.
do_check_true(onchangeNotified);
do_check_true(lastNotifiedDone);
do_check_true(lastNotifiedStopped);
do_check_eq(lastNotifiedProgress, 100);
});
@ -89,30 +104,9 @@ add_task(function test_download_view_final_notified()
*/
add_task(function test_download_intermediate_progress()
{
let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
let interruptible = startInterruptibleResponseHandler();
let deferUntilHalfProgress = Promise.defer();
gHttpServer.registerPathHandler("/test_download_intermediate_progress",
function (aRequest, aResponse)
{
aResponse.processAsync();
aResponse.setHeader("Content-Type", "text/plain", false);
aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
false);
aResponse.write(TEST_DATA_SHORT);
deferUntilHalfProgress.promise.then(function () {
aResponse.write(TEST_DATA_SHORT);
aResponse.finish();
});
});
let download = yield Downloads.createDownload({
source: { uri: NetUtil.newURI(HTTP_BASE +
"/test_download_intermediate_progress") },
target: { file: targetFile },
saver: { type: "copy" },
});
let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
download.onchange = function () {
if (download.progress == 50) {
@ -121,17 +115,82 @@ add_task(function test_download_intermediate_progress()
do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
// Continue after the first chunk of data is fully received.
deferUntilHalfProgress.resolve();
interruptible.resolve();
}
};
// Starts the download and waits for completion.
yield download.start();
do_check_true(download.done);
do_check_true(download.stopped);
do_check_eq(download.progress, 100);
yield promiseVerifyContents(targetFile, TEST_DATA_SHORT + TEST_DATA_SHORT);
yield promiseVerifyContents(download.target.file,
TEST_DATA_SHORT + TEST_DATA_SHORT);
});
/**
* Cancels a download and verifies that its state is reported correctly.
*/
add_task(function test_download_cancel()
{
let interruptible = startInterruptibleResponseHandler();
try {
let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
// Cancel the download after receiving the first part of the response.
download.onchange = function () {
if (!download.stopped && download.progress == 50) {
download.cancel();
}
};
try {
yield download.start();
do_throw("The download should have been canceled.");
} catch (ex if ex instanceof Downloads.Error) {
do_check_false(ex.becauseSourceFailed);
do_check_false(ex.becauseTargetFailed);
}
do_check_true(download.stopped);
do_check_true(download.canceled);
do_check_true(download.error === null);
do_check_false(download.target.file.exists());
} finally {
interruptible.reject();
}
});
/**
* Cancels a download right after starting it.
*/
add_task(function test_download_cancel_immediately()
{
let interruptible = startInterruptibleResponseHandler();
try {
let download = yield promiseSimpleDownload(TEST_INTERRUPTIBLE_URI);
let promiseStopped = download.start();
download.cancel();
try {
yield promiseStopped;
do_throw("The download should have been canceled.");
} catch (ex if ex instanceof Downloads.Error) {
do_check_false(ex.becauseSourceFailed);
do_check_false(ex.becauseTargetFailed);
}
do_check_true(download.stopped);
do_check_true(download.canceled);
do_check_true(download.error === null);
do_check_false(download.target.file.exists());
} finally {
interruptible.reject();
}
});
/**
@ -139,15 +198,9 @@ add_task(function test_download_intermediate_progress()
*/
add_task(function test_download_error_source()
{
let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
let serverSocket = startFakeServer();
try {
let download = yield Downloads.createDownload({
source: { uri: TEST_FAKE_SOURCE_URI },
target: { file: targetFile },
saver: { type: "copy" },
});
let download = yield promiseSimpleDownload(TEST_FAKE_SOURCE_URI);
do_check_true(download.error === null);
@ -158,7 +211,8 @@ add_task(function test_download_error_source()
// A specific error object is thrown when reading from the source fails.
}
do_check_true(download.done);
do_check_true(download.stopped);
do_check_false(download.canceled);
do_check_true(download.error !== null);
do_check_true(download.error.becauseSourceFailed);
do_check_false(download.error.becauseTargetFailed);
@ -172,19 +226,13 @@ add_task(function test_download_error_source()
*/
add_task(function test_download_error_target()
{
let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
// Create a file without write access permissions.
targetFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
let download = yield Downloads.createDownload({
source: { uri: TEST_SOURCE_URI },
target: { file: targetFile },
saver: { type: "copy" },
});
let download = yield promiseSimpleDownload();
do_check_true(download.error === null);
// Create a file without write access permissions before downloading.
download.target.file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
try {
yield download.start();
do_throw("The download should have failed.");
@ -192,7 +240,8 @@ add_task(function test_download_error_target()
// A specific error object is thrown when writing to the target fails.
}
do_check_true(download.done);
do_check_true(download.stopped);
do_check_false(download.canceled);
do_check_true(download.error !== null);
do_check_true(download.error.becauseTargetFailed);
do_check_false(download.error.becauseSourceFailed);