mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 836437 - Part 2 of 3 - Add the ability to resume a download from where it stopped to DownloadCopySaver. r=enn
This commit is contained in:
parent
f275aa42e0
commit
dd4f927547
@ -183,6 +183,19 @@ Download.prototype = {
|
||||
*/
|
||||
currentBytes: 0,
|
||||
|
||||
/**
|
||||
* Indicates whether, at this time, there is any partially downloaded data
|
||||
* that can be used when restarting a failed or canceled download.
|
||||
*
|
||||
* This property is relevant while the download is in progress, and also if it
|
||||
* failed or has been canceled. If the download has been completed
|
||||
* successfully, this property is not relevant anymore.
|
||||
*
|
||||
* Whether partial data can actually be retained depends on the saver and the
|
||||
* download source, and may not be known before the download is started.
|
||||
*/
|
||||
hasPartialData: false,
|
||||
|
||||
/**
|
||||
* This can be set to a function that is called after other properties change.
|
||||
*/
|
||||
@ -266,6 +279,13 @@ Download.prototype = {
|
||||
return this._currentAttempt;
|
||||
}
|
||||
|
||||
// While shutting down or disposing of this object, we prevent the download
|
||||
// from returning to be in progress.
|
||||
if (this._finalized) {
|
||||
return Promise.reject(new DownloadError(Cr.NS_ERROR_FAILURE,
|
||||
"Cannot start after finalization."));
|
||||
}
|
||||
|
||||
// Initialize all the status properties for a new or restarted download.
|
||||
this.stopped = false;
|
||||
this.canceled = false;
|
||||
@ -284,10 +304,10 @@ Download.prototype = {
|
||||
|
||||
// This function propagates progress from the DownloadSaver object, unless
|
||||
// it comes in late from a download attempt that was replaced by a new one.
|
||||
function DS_setProgressBytes(aCurrentBytes, aTotalBytes)
|
||||
function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData)
|
||||
{
|
||||
if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
|
||||
this._setBytes(aCurrentBytes, aTotalBytes);
|
||||
this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData);
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,10 +335,18 @@ Download.prototype = {
|
||||
// Now that we stored the promise in the download object, we can start the
|
||||
// task that will actually execute the download.
|
||||
deferAttempt.resolve(Task.spawn(function task_D_start() {
|
||||
// Wait upon any pending cancellation request.
|
||||
// Wait upon any pending operation before restarting.
|
||||
if (this._promiseCanceled) {
|
||||
yield this._promiseCanceled;
|
||||
}
|
||||
if (this._promiseRemovePartialData) {
|
||||
try {
|
||||
yield this._promiseRemovePartialData;
|
||||
} catch (ex) {
|
||||
// Ignore any errors, which are already reported by the original
|
||||
// caller of the removePartialData method.
|
||||
}
|
||||
}
|
||||
|
||||
// Disallow download if parental controls service restricts it.
|
||||
if (yield DownloadIntegration.shouldBlockForParentalControls(this)) {
|
||||
@ -473,6 +501,83 @@ Download.prototype = {
|
||||
return this._promiseCanceled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates whether any partially downloaded data should be retained, to use
|
||||
* when restarting a failed or canceled download. The default is false.
|
||||
*
|
||||
* Whether partial data can actually be retained depends on the saver and the
|
||||
* download source, and may not be known before the download is started.
|
||||
*
|
||||
* To have any effect, this property must be set before starting the download.
|
||||
* Resetting this property to false after the download has already started
|
||||
* will not remove any partial data.
|
||||
*
|
||||
* If this property is set to true, care should be taken that partial data is
|
||||
* removed before the reference to the download is discarded. This can be
|
||||
* done using the removePartialData or the "finalize" methods.
|
||||
*/
|
||||
tryToKeepPartialData: false,
|
||||
|
||||
/**
|
||||
* When a request to remove partially downloaded data is received, contains a
|
||||
* promise that will be resolved when the removal request is processed. When
|
||||
* the request is processed, this property becomes null again.
|
||||
*/
|
||||
_promiseRemovePartialData: null,
|
||||
|
||||
/**
|
||||
* Removes any partial data kept as part of a canceled or failed download.
|
||||
*
|
||||
* If the download is not canceled or failed, this method has no effect, and
|
||||
* it returns a resolved promise. If the "cancel" method was called but the
|
||||
* cancellation process has not finished yet, this method waits for the
|
||||
* cancellation to finish, then removes the partial data.
|
||||
*
|
||||
* After this method has been called, if the tryToKeepPartialData property is
|
||||
* still true when the download is restarted, partial data will be retained
|
||||
* during the new download attempt.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the partial data has been successfully removed.
|
||||
* @rejects JavaScript exception if the operation could not be completed.
|
||||
*/
|
||||
removePartialData: function ()
|
||||
{
|
||||
if (!this.canceled && !this.error) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let promiseRemovePartialData = this._promiseRemovePartialData;
|
||||
|
||||
if (!promiseRemovePartialData) {
|
||||
let deferRemovePartialData = Promise.defer();
|
||||
promiseRemovePartialData = deferRemovePartialData.promise;
|
||||
this._promiseRemovePartialData = promiseRemovePartialData;
|
||||
|
||||
deferRemovePartialData.resolve(
|
||||
Task.spawn(function task_D_removePartialData() {
|
||||
try {
|
||||
// Wait upon any pending cancellation request.
|
||||
if (this._promiseCanceled) {
|
||||
yield this._promiseCanceled;
|
||||
}
|
||||
// Ask the saver object to remove any partial data.
|
||||
yield this.saver.removePartialData();
|
||||
// For completeness, clear the number of bytes transferred.
|
||||
if (this.currentBytes != 0 || this.hasPartialData) {
|
||||
this.currentBytes = 0;
|
||||
this.hasPartialData = false;
|
||||
this._notifyChange();
|
||||
}
|
||||
} finally {
|
||||
this._promiseRemovePartialData = null;
|
||||
}
|
||||
}.bind(this)));
|
||||
}
|
||||
|
||||
return promiseRemovePartialData;
|
||||
},
|
||||
|
||||
/**
|
||||
* This deferred object contains a promise that is resolved as soon as this
|
||||
* download finishes successfully, and is never rejected. This property is
|
||||
@ -498,6 +603,48 @@ Download.prototype = {
|
||||
return this._deferSucceeded.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* True if the "finalize" method has been called. This prevents the download
|
||||
* from starting again after having been stopped.
|
||||
*/
|
||||
_finalized: false,
|
||||
|
||||
/**
|
||||
* Ensures that the download is stopped, and optionally removes any partial
|
||||
* data kept as part of a canceled or failed download. After this method has
|
||||
* been called, the download cannot be started again.
|
||||
*
|
||||
* This method should be used in place of "cancel" and removePartialData while
|
||||
* shutting down or disposing of the download object, to prevent other callers
|
||||
* from interfering with the operation. This is required because cancellation
|
||||
* and other operations are asynchronous.
|
||||
*
|
||||
* @param aRemovePartialData
|
||||
* Whether any partially downloaded data should be removed after the
|
||||
* download has been stopped.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation has finished successfully.
|
||||
* @rejects JavaScript exception if an error occurred while removing the
|
||||
* partially downloaded data.
|
||||
*/
|
||||
finalize: function (aRemovePartialData)
|
||||
{
|
||||
// Prevents the download from starting again after having been stopped.
|
||||
this._finalized = true;
|
||||
|
||||
if (aRemovePartialData) {
|
||||
// Cancel the download, in case it is currently in progress, then remove
|
||||
// any partially downloaded data. The removal operation waits for
|
||||
// cancellation to be completed before resolving the promise it returns.
|
||||
this.cancel();
|
||||
return this.removePartialData();
|
||||
} else {
|
||||
// Just cancel the download, in case it is currently in progress.
|
||||
return this.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates progress notifications based on the number of bytes transferred.
|
||||
*
|
||||
@ -505,9 +652,13 @@ Download.prototype = {
|
||||
* Number of bytes transferred until now.
|
||||
* @param aTotalBytes
|
||||
* Total number of bytes to be transferred, or -1 if unknown.
|
||||
* @param aHasPartialData
|
||||
* Indicates whether the partially downloaded data can be used when
|
||||
* restarting the download if it fails or is canceled.
|
||||
*/
|
||||
_setBytes: function D_setBytes(aCurrentBytes, aTotalBytes) {
|
||||
_setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
|
||||
this.currentBytes = aCurrentBytes;
|
||||
this.hasPartialData = aHasPartialData;
|
||||
if (aTotalBytes != -1) {
|
||||
this.hasProgress = true;
|
||||
this.totalBytes = aTotalBytes;
|
||||
@ -680,11 +831,13 @@ DownloadSource.prototype = {
|
||||
DownloadSource.fromSerializable = function (aSerializable) {
|
||||
let source = new DownloadSource();
|
||||
if (isString(aSerializable)) {
|
||||
source.url = aSerializable;
|
||||
// Convert String objects to primitive strings at this point.
|
||||
source.url = aSerializable.toString();
|
||||
} else if (aSerializable instanceof Ci.nsIURI) {
|
||||
source.url = aSerializable.spec;
|
||||
} else {
|
||||
source.url = aSerializable.url;
|
||||
// Convert String objects to primitive strings at this point.
|
||||
source.url = aSerializable.url.toString();
|
||||
if ("isPrivate" in aSerializable) {
|
||||
source.isPrivate = aSerializable.isPrivate;
|
||||
}
|
||||
@ -710,6 +863,13 @@ DownloadTarget.prototype = {
|
||||
*/
|
||||
path: null,
|
||||
|
||||
/**
|
||||
* String containing the path of the ".part" file containing the data
|
||||
* downloaded so far, or null to disable the use of a ".part" file to keep
|
||||
* partially downloaded data.
|
||||
*/
|
||||
partFilePath: null,
|
||||
|
||||
/**
|
||||
* Returns a static representation of the current object state.
|
||||
*
|
||||
@ -717,8 +877,13 @@ DownloadTarget.prototype = {
|
||||
*/
|
||||
toSerializable: function ()
|
||||
{
|
||||
// Simplify the representation since we don't have other details for now.
|
||||
// Simplify the representation if we don't have other details.
|
||||
if (!this.partFilePath) {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
return { path: this.path,
|
||||
partFilePath: this.partFilePath };
|
||||
},
|
||||
};
|
||||
|
||||
@ -738,14 +903,18 @@ DownloadTarget.prototype = {
|
||||
DownloadTarget.fromSerializable = function (aSerializable) {
|
||||
let target = new DownloadTarget();
|
||||
if (isString(aSerializable)) {
|
||||
target.path = aSerializable;
|
||||
// Convert String objects to primitive strings at this point.
|
||||
target.path = aSerializable.toString();
|
||||
} else if (aSerializable instanceof Ci.nsIFile) {
|
||||
// Read the "path" property of nsIFile after checking the object type.
|
||||
target.path = aSerializable.path;
|
||||
} else {
|
||||
// Read the "path" property of the serializable DownloadTarget
|
||||
// representation.
|
||||
target.path = aSerializable.path;
|
||||
// representation, converting String objects to primitive strings.
|
||||
target.path = aSerializable.path.toString();
|
||||
if ("partFilePath" in aSerializable) {
|
||||
target.partFilePath = aSerializable.partFilePath;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
@ -787,6 +956,7 @@ function DownloadError(aResult, aMessage, aInferCause)
|
||||
this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK);
|
||||
this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES);
|
||||
}
|
||||
this.stack = new Error().stack;
|
||||
}
|
||||
|
||||
DownloadError.prototype = {
|
||||
@ -831,6 +1001,10 @@ function DownloadSaver() { }
|
||||
DownloadSaver.prototype = {
|
||||
/**
|
||||
* Download object for raising notifications and reading properties.
|
||||
*
|
||||
* If the tryToKeepPartialData property of the download object is false, the
|
||||
* saver should never try to keep partially downloaded data if the download
|
||||
* fails.
|
||||
*/
|
||||
download: null,
|
||||
|
||||
@ -839,9 +1013,11 @@ DownloadSaver.prototype = {
|
||||
*
|
||||
* @param aSetProgressBytesFn
|
||||
* This function may be called by the saver to report progress. It
|
||||
* takes two arguments: the first is the number of bytes transferred
|
||||
* takes three arguments: the first is the number of bytes transferred
|
||||
* until now, the second is the total number of bytes to be
|
||||
* transferred, or -1 if unknown.
|
||||
* transferred (or -1 if unknown), the third indicates whether the
|
||||
* partially downloaded data can be used when restarting the download
|
||||
* if it fails or is canceled.
|
||||
* @parem aSetPropertiesFn
|
||||
* This function may be called by the saver to report information
|
||||
* about new download properties discovered by the saver during the
|
||||
@ -866,6 +1042,22 @@ DownloadSaver.prototype = {
|
||||
throw new Error("Not implemented.");
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes any partial data kept as part of a canceled or failed download.
|
||||
*
|
||||
* This method is never called until the promise returned by "execute" is
|
||||
* either resolved or rejected, and the "execute" method is not called again
|
||||
* until the promise returned by this method is resolved or rejected.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation has finished successfully.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
removePartialData: function DS_removePartialData()
|
||||
{
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a static representation of the current object state.
|
||||
*
|
||||
@ -920,13 +1112,54 @@ DownloadCopySaver.prototype = {
|
||||
*/
|
||||
_backgroundFileSaver: null,
|
||||
|
||||
/**
|
||||
* Indicates whether the "cancel" method has been called. This is used to
|
||||
* prevent the request from starting in case the operation is canceled before
|
||||
* the BackgroundFileSaver instance has been created.
|
||||
*/
|
||||
_canceled: false,
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.execute".
|
||||
*/
|
||||
execute: function DCS_execute(aSetProgressBytesFn, aSetPropertiesFn)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let copySaver = this;
|
||||
|
||||
this._canceled = false;
|
||||
|
||||
let download = this.download;
|
||||
let targetPath = download.target.path;
|
||||
let partFilePath = download.target.partFilePath;
|
||||
let keepPartialData = download.tryToKeepPartialData;
|
||||
|
||||
return Task.spawn(function task_DCS_execute() {
|
||||
// To reduce the chance that other downloads reuse the same final target
|
||||
// file name, we should create a placeholder as soon as possible, before
|
||||
// starting the network request. The placeholder is also required in case
|
||||
// we are using a ".part" file instead of the final target while the
|
||||
// download is in progress.
|
||||
try {
|
||||
// If the file already exists, don't delete its contents yet.
|
||||
let file = yield OS.File.open(targetPath, { write: true });
|
||||
yield file.close();
|
||||
} catch (ex if ex instanceof OS.File.Error) {
|
||||
// Throw a DownloadError indicating that the operation failed because of
|
||||
// the target file. We cannot translate this into a specific result
|
||||
// code, but we preserve the original message using the toString method.
|
||||
let error = new DownloadError(Cr.NS_ERROR_FAILURE, ex.toString());
|
||||
error.becauseTargetFailed = true;
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
let deferSaveComplete = Promise.defer();
|
||||
|
||||
if (this._canceled) {
|
||||
// Don't create the BackgroundFileSaver object if we have been
|
||||
// canceled meanwhile.
|
||||
throw new DownloadError(Cr.NS_ERROR_FAILURE, "Saver canceled.");
|
||||
}
|
||||
|
||||
// Create the object that will save the file in a background thread.
|
||||
let backgroundFileSaver = new BackgroundFileSaverStreamListener();
|
||||
@ -937,41 +1170,54 @@ DownloadCopySaver.prototype = {
|
||||
onTargetChange: function () { },
|
||||
onSaveComplete: function DCSE_onSaveComplete(aSaver, aStatus)
|
||||
{
|
||||
// Free the reference cycle, in order to release resources earlier.
|
||||
// Free the reference cycle, to release resources earlier.
|
||||
backgroundFileSaver.observer = null;
|
||||
this._backgroundFileSaver = null;
|
||||
|
||||
// Send notifications now that we can restart the download if needed.
|
||||
// Send notifications now that we can restart if needed.
|
||||
if (Components.isSuccessCode(aStatus)) {
|
||||
deferred.resolve();
|
||||
deferSaveComplete.resolve();
|
||||
} else {
|
||||
// Infer the origin of the error from the failure code, because
|
||||
// BackgroundFileSaver does not provide more specific data.
|
||||
deferred.reject(new DownloadError(aStatus, null, true));
|
||||
deferSaveComplete.reject(new DownloadError(aStatus, null,
|
||||
true));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Set the target file, that will be deleted if the download fails.
|
||||
backgroundFileSaver.setTarget(new FileUtils.File(download.target.path),
|
||||
false);
|
||||
|
||||
// Create a channel from the source, and listen to progress notifications.
|
||||
// Create a channel from the source, and listen to progress
|
||||
// notifications.
|
||||
let channel = NetUtil.newChannel(NetUtil.newURI(download.source.url));
|
||||
if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
|
||||
channel.setPrivate(download.source.isPrivate);
|
||||
}
|
||||
if (channel instanceof Ci.nsIHttpChannel && download.source.referrer) {
|
||||
if (channel instanceof Ci.nsIHttpChannel &&
|
||||
download.source.referrer) {
|
||||
channel.referrer = NetUtil.newURI(download.source.referrer);
|
||||
}
|
||||
|
||||
// If we have data that we can use to resume the download from where
|
||||
// it stopped, try to use it.
|
||||
let resumeAttempted = false;
|
||||
if (channel instanceof Ci.nsIResumableChannel && this.entityID &&
|
||||
partFilePath && keepPartialData) {
|
||||
try {
|
||||
let stat = yield OS.File.stat(partFilePath);
|
||||
channel.resumeAt(stat.size, this.entityID);
|
||||
resumeAttempted = true;
|
||||
} catch (ex if ex instanceof OS.File.Error &&
|
||||
ex.becauseNoSuchFile) { }
|
||||
}
|
||||
|
||||
channel.notificationCallbacks = {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
|
||||
getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]),
|
||||
onProgress: function DCSE_onProgress(aRequest, aContext, aProgress,
|
||||
aProgressMax)
|
||||
{
|
||||
aSetProgressBytesFn(aProgress, aProgressMax);
|
||||
aSetProgressBytesFn(aProgress, aProgressMax, aProgress > 0 &&
|
||||
partFilePath && keepPartialData);
|
||||
},
|
||||
onStatus: function () { },
|
||||
};
|
||||
@ -979,50 +1225,100 @@ DownloadCopySaver.prototype = {
|
||||
// Open the channel, directing output to the background file saver.
|
||||
backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
|
||||
channel.asyncOpen({
|
||||
onStartRequest: function DCSE_onStartRequest(aRequest, aContext)
|
||||
{
|
||||
onStartRequest: function (aRequest, aContext) {
|
||||
backgroundFileSaver.onStartRequest(aRequest, aContext);
|
||||
|
||||
// Ensure we report the value of "Content-Length", if available, even
|
||||
// if the download doesn't generate any progress events later.
|
||||
// Ensure we report the value of "Content-Length", if available,
|
||||
// even if the download doesn't generate any progress events
|
||||
// later.
|
||||
if (aRequest instanceof Ci.nsIChannel &&
|
||||
aRequest.contentLength >= 0) {
|
||||
aSetProgressBytesFn(0, aRequest.contentLength);
|
||||
aSetPropertiesFn({ contentType: aRequest.contentType });
|
||||
}
|
||||
},
|
||||
onStopRequest: function DCSE_onStopRequest(aRequest, aContext,
|
||||
aStatusCode)
|
||||
{
|
||||
|
||||
if (keepPartialData) {
|
||||
// If the source is not resumable, don't keep partial data even
|
||||
// if we were asked to try and do it.
|
||||
if (aRequest instanceof Ci.nsIResumableChannel) {
|
||||
try {
|
||||
backgroundFileSaver.onStopRequest(aRequest, aContext, aStatusCode);
|
||||
// If reading the ID succeeds, the source is resumable.
|
||||
this.entityID = aRequest.entityID;
|
||||
} catch (ex if ex instanceof Components.Exception &&
|
||||
ex.result == Cr.NS_ERROR_NOT_RESUMABLE) {
|
||||
keepPartialData = false;
|
||||
}
|
||||
} else {
|
||||
keepPartialData = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (partFilePath) {
|
||||
// If we actually resumed a request, append to the partial data.
|
||||
if (resumeAttempted) {
|
||||
// TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED
|
||||
backgroundFileSaver.enableAppend();
|
||||
}
|
||||
|
||||
// Use a part file, determining if we should keep it on failure.
|
||||
backgroundFileSaver.setTarget(new FileUtils.File(partFilePath),
|
||||
keepPartialData);
|
||||
} else {
|
||||
// Set the final target file, and delete it on failure.
|
||||
backgroundFileSaver.setTarget(new FileUtils.File(targetPath),
|
||||
false);
|
||||
}
|
||||
}.bind(copySaver),
|
||||
|
||||
onStopRequest: function (aRequest, aContext, aStatusCode) {
|
||||
try {
|
||||
backgroundFileSaver.onStopRequest(aRequest, aContext,
|
||||
aStatusCode);
|
||||
} finally {
|
||||
// If the data transfer completed successfully, indicate to the
|
||||
// background file saver that the operation can finish. If the
|
||||
// data transfer failed, the saver has been already stopped.
|
||||
if (Components.isSuccessCode(aStatusCode)) {
|
||||
if (partFilePath) {
|
||||
// Move to the final target if we were using a part file.
|
||||
backgroundFileSaver.setTarget(
|
||||
new FileUtils.File(targetPath), false);
|
||||
}
|
||||
backgroundFileSaver.finish(Cr.NS_OK);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDataAvailable: function DCSE_onDataAvailable(aRequest, aContext,
|
||||
}.bind(copySaver),
|
||||
|
||||
onDataAvailable: function (aRequest, aContext, aInputStream,
|
||||
aOffset, aCount) {
|
||||
backgroundFileSaver.onDataAvailable(aRequest, aContext,
|
||||
aInputStream, aOffset,
|
||||
aCount)
|
||||
{
|
||||
backgroundFileSaver.onDataAvailable(aRequest, aContext, aInputStream,
|
||||
aOffset, aCount);
|
||||
},
|
||||
aCount);
|
||||
}.bind(copySaver),
|
||||
}, 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.
|
||||
deferred.reject(ex);
|
||||
// In case an error occurs while setting up the chain of objects for
|
||||
// the download, ensure that we release the resources of the saver.
|
||||
backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
|
||||
throw ex;
|
||||
}
|
||||
return deferred.promise;
|
||||
|
||||
// We will wait on this promise in case no error occurred while setting
|
||||
// up the chain of objects for the download.
|
||||
yield deferSaveComplete.promise;
|
||||
} catch (ex) {
|
||||
// Ensure we always remove the placeholder for the final target file on
|
||||
// failure, independently of which code path failed. In some cases, the
|
||||
// background file saver may have already removed the file.
|
||||
try {
|
||||
yield OS.File.remove(targetPath);
|
||||
} catch (e2 if e2 instanceof OS.File.Error && e2.becauseNoSuchFile) { }
|
||||
throw ex;
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1030,12 +1326,27 @@ DownloadCopySaver.prototype = {
|
||||
*/
|
||||
cancel: function DCS_cancel()
|
||||
{
|
||||
this._canceled = true;
|
||||
if (this._backgroundFileSaver) {
|
||||
this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
|
||||
this._backgroundFileSaver = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.removePartialData".
|
||||
*/
|
||||
removePartialData: function ()
|
||||
{
|
||||
return Task.spawn(function task_DCS_removePartialData() {
|
||||
if (this.download.target.partFilePath) {
|
||||
try {
|
||||
yield OS.File.remove(this.download.target.partFilePath);
|
||||
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { }
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.toSerializable".
|
||||
*/
|
||||
|
@ -104,6 +104,11 @@ DownloadList.prototype = {
|
||||
* Removes a download from the list. If the download was already removed,
|
||||
* this method has no effect.
|
||||
*
|
||||
* This method does not change the state of the download, to allow adding it
|
||||
* to another list, or control it directly. If you want to dispose of the
|
||||
* download object, you should cancel it afterwards, and remove any partially
|
||||
* downloaded data if needed.
|
||||
*
|
||||
* @param aDownload
|
||||
* The Download object to remove.
|
||||
*/
|
||||
@ -208,7 +213,14 @@ DownloadList.prototype = {
|
||||
// operation hasn't completed yet so we don't check "stopped" here.
|
||||
if ((download.succeeded || download.canceled || download.error) &&
|
||||
aTestFn(download)) {
|
||||
// Remove the download first, so that the views don't get the change
|
||||
// notifications that may occur during finalization.
|
||||
this.remove(download);
|
||||
// Ensure that the download is stopped and no partial data is kept.
|
||||
// This works even if the download state has changed meanwhile. We
|
||||
// don't need to wait for the procedure to be complete before
|
||||
// processing the other downloads in the list.
|
||||
download.finalize(true);
|
||||
}
|
||||
}
|
||||
}.bind(this)).then(null, Cu.reportError);
|
||||
|
@ -90,9 +90,12 @@ this.Downloads = {
|
||||
/**
|
||||
* Downloads data from a remote network location to a local file.
|
||||
*
|
||||
* This download method does not provide user interface or the ability to
|
||||
* cancel the download programmatically. For that, you should obtain a
|
||||
* reference to a Download object using the createDownload function.
|
||||
* This download method does not provide user interface, or the ability to
|
||||
* cancel or restart the download programmatically. For that, you should
|
||||
* obtain a reference to a Download object using the createDownload function.
|
||||
*
|
||||
* Since the download cannot be restarted, any partially downloaded data will
|
||||
* not be kept in case the download fails.
|
||||
*
|
||||
* @param aSource
|
||||
* String containing the URI for the download source. Alternatively,
|
||||
|
@ -35,6 +35,36 @@ function promiseStartDownload(aSourceUrl) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a download to reach half of its progress, in case it has not
|
||||
* reached the expected progress already.
|
||||
*
|
||||
* @param aDownload
|
||||
* The Download object to wait upon.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the download has reached half of its progress.
|
||||
* @rejects Never.
|
||||
*/
|
||||
function promiseDownloadMidway(aDownload) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
// Wait for the download to reach half of its progress.
|
||||
let onchange = function () {
|
||||
if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
|
||||
aDownload.onchange = null;
|
||||
deferred.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Register for the notification, but also call the function directly in
|
||||
// case the download already reached the expected progress.
|
||||
aDownload.onchange = onchange;
|
||||
onchange();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a download to finish, in case it has not finished already.
|
||||
*
|
||||
@ -60,6 +90,53 @@ function promiseDownloadStopped(aDownload) {
|
||||
return Promise.reject(aDownload.error || new Error("Download canceled."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and starts a new download, configured to keep partial data, and
|
||||
* returns only when the first part of "interruptible_resumable.txt" has been
|
||||
* saved to disk. You must call "continueResponses" to allow the interruptible
|
||||
* request to continue.
|
||||
*
|
||||
* TODO: This function uses either DownloadCopySaver or DownloadLegacySaver
|
||||
* based on the current test run.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The newly created Download object, still in progress.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
function promiseStartDownload_tryToKeepPartialData() {
|
||||
return Task.spawn(function () {
|
||||
mustInterruptResponses();
|
||||
|
||||
// Start a new download and configure it to keep partially downloaded data.
|
||||
let targetFilePath = getTempFile(TEST_TARGET_FILE_NAME).path;
|
||||
let download = yield Downloads.createDownload({
|
||||
source: httpUrl("interruptible_resumable.txt"),
|
||||
target: { path: targetFilePath,
|
||||
partFilePath: targetFilePath + ".part" },
|
||||
});
|
||||
download.tryToKeepPartialData = true;
|
||||
download.start();
|
||||
|
||||
yield promiseDownloadMidway(download);
|
||||
|
||||
// After we receive the progress notification, we should wait for the worker
|
||||
// thread of BackgroundFileSaver to receive the data to be written to disk.
|
||||
// We don't have control over when this happens. We may only check that the
|
||||
// ".part" file has been created, while its size cannot be determined because
|
||||
// the file is currently open.
|
||||
try {
|
||||
while (!(yield OS.File.exists(download.target.partFilePath))) {
|
||||
yield promiseTimeout(50);
|
||||
}
|
||||
} catch (ex if ex instanceof OS.File.Error) {
|
||||
// This indicates that the file has been created and cannot be accessed.
|
||||
// The specific error might vary with the platform.
|
||||
}
|
||||
|
||||
throw new Task.Result(download);
|
||||
});
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Tests
|
||||
|
||||
@ -116,7 +193,6 @@ add_task(function test_referrer()
|
||||
function cleanup() {
|
||||
gHttpServer.registerPathHandler(sourcePath, null);
|
||||
}
|
||||
|
||||
do_register_cleanup(cleanup);
|
||||
|
||||
gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
|
||||
@ -191,7 +267,7 @@ add_task(function test_initial_final_state()
|
||||
*/
|
||||
add_task(function test_final_state_notified()
|
||||
{
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
@ -206,7 +282,7 @@ add_task(function test_final_state_notified()
|
||||
|
||||
// Allow the download to complete.
|
||||
let promiseAttempt = download.start();
|
||||
deferResponse.resolve();
|
||||
continueResponses();
|
||||
yield promiseAttempt;
|
||||
|
||||
// The view should have been notified before the download completes.
|
||||
@ -220,26 +296,18 @@ add_task(function test_final_state_notified()
|
||||
*/
|
||||
add_task(function test_intermediate_progress()
|
||||
{
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let onchange = function () {
|
||||
if (download.progress == 50) {
|
||||
yield promiseDownloadMidway(download);
|
||||
|
||||
do_check_true(download.hasProgress);
|
||||
do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
|
||||
do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
|
||||
|
||||
// Continue after the first chunk of data is fully received.
|
||||
deferResponse.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Register for the notification, but also call the function directly in case
|
||||
// the download already reached the expected progress.
|
||||
download.onchange = onchange;
|
||||
onchange();
|
||||
|
||||
continueResponses();
|
||||
yield promiseDownloadStopped(download);
|
||||
|
||||
do_check_true(download.stopped);
|
||||
@ -271,15 +339,30 @@ add_task(function test_empty_progress()
|
||||
*/
|
||||
add_task(function test_empty_noprogress()
|
||||
{
|
||||
let deferResponse = deferNextResponse();
|
||||
let promiseEmptyRequestReceived = promiseNextRequestReceived();
|
||||
let sourcePath = "/test_empty_noprogress.txt";
|
||||
let sourceUrl = httpUrl("test_empty_noprogress.txt");
|
||||
let deferRequestReceived = Promise.defer();
|
||||
|
||||
// Register an interruptible handler that notifies us when the request occurs.
|
||||
function cleanup() {
|
||||
gHttpServer.registerPathHandler(sourcePath, null);
|
||||
}
|
||||
do_register_cleanup(cleanup);
|
||||
|
||||
registerInterruptibleHandler(sourcePath,
|
||||
function firstPart(aRequest, aResponse) {
|
||||
aResponse.setHeader("Content-Type", "text/plain", false);
|
||||
deferRequestReceived.resolve();
|
||||
}, function secondPart(aRequest, aResponse) { });
|
||||
|
||||
// Start the download, without allowing the request to finish.
|
||||
mustInterruptResponses();
|
||||
let download;
|
||||
if (!gUseLegacySaver) {
|
||||
// When testing DownloadCopySaver, we have control over the download, thus
|
||||
// we can hook its onchange callback that will be notified when the
|
||||
// download starts.
|
||||
download = yield promiseNewDownload(httpUrl("empty-noprogress.txt"));
|
||||
download = yield promiseNewDownload(sourceUrl);
|
||||
|
||||
download.onchange = function () {
|
||||
if (!download.stopped) {
|
||||
@ -294,14 +377,13 @@ add_task(function test_empty_noprogress()
|
||||
// When testing DownloadLegacySaver, the download is already started when it
|
||||
// is created, and it may have already made all needed property change
|
||||
// notifications, thus there is no point in checking the onchange callback.
|
||||
download = yield promiseStartLegacyDownload(
|
||||
httpUrl("empty-noprogress.txt"));
|
||||
download = yield promiseStartLegacyDownload(sourceUrl);
|
||||
}
|
||||
|
||||
// Wait for the request to be received by the HTTP server, but don't allow the
|
||||
// request to finish yet. Before checking the download state, wait for the
|
||||
// events to be processed by the client.
|
||||
yield promiseEmptyRequestReceived;
|
||||
yield deferRequestReceived.promise;
|
||||
yield promiseExecuteSoon();
|
||||
|
||||
// Check that this download has no progress report.
|
||||
@ -311,7 +393,7 @@ add_task(function test_empty_noprogress()
|
||||
do_check_eq(download.totalBytes, 0);
|
||||
|
||||
// Now allow the response to finish.
|
||||
deferResponse.resolve();
|
||||
continueResponses();
|
||||
yield promiseDownloadStopped(download);
|
||||
|
||||
// Verify the state of the completed download.
|
||||
@ -329,8 +411,7 @@ add_task(function test_empty_noprogress()
|
||||
*/
|
||||
add_task(function test_start_twice()
|
||||
{
|
||||
// Ensure that the download cannot complete before start is called twice.
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download;
|
||||
if (!gUseLegacySaver) {
|
||||
@ -348,7 +429,7 @@ add_task(function test_start_twice()
|
||||
let promiseAttempt2 = download.start();
|
||||
|
||||
// Allow the download to finish.
|
||||
deferResponse.resolve();
|
||||
continueResponses();
|
||||
|
||||
// Both promises should now be resolved.
|
||||
yield promiseAttempt1;
|
||||
@ -368,7 +449,7 @@ add_task(function test_start_twice()
|
||||
*/
|
||||
add_task(function test_cancel_midway()
|
||||
{
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
// In this test case, we execute different checks that are only possible with
|
||||
// DownloadCopySaver or DownloadLegacySaver respectively.
|
||||
@ -381,11 +462,11 @@ add_task(function test_cancel_midway()
|
||||
options);
|
||||
}
|
||||
|
||||
try {
|
||||
// Cancel the download after receiving the first part of the response.
|
||||
let deferCancel = Promise.defer();
|
||||
let onchange = function () {
|
||||
if (!download.stopped && !download.canceled && download.progress == 50) {
|
||||
// Cancel the download immediately during the notification.
|
||||
deferCancel.resolve(download.cancel());
|
||||
|
||||
// The state change happens immediately after calling "cancel", but
|
||||
@ -435,9 +516,6 @@ add_task(function test_cancel_midway()
|
||||
do_check_false(ex.becauseTargetFailed);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@ -445,9 +523,8 @@ add_task(function test_cancel_midway()
|
||||
*/
|
||||
add_task(function test_cancel_immediately()
|
||||
{
|
||||
// Ensure that the download cannot complete before cancel is called.
|
||||
let deferResponse = deferNextResponse();
|
||||
try {
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let promiseAttempt = download.start();
|
||||
@ -475,17 +552,6 @@ add_task(function test_cancel_immediately()
|
||||
|
||||
// Check that the promise returned by the "cancel" method has been resolved.
|
||||
yield promiseCancel;
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
}
|
||||
|
||||
// Even if we canceled the download immediately, the HTTP request might have
|
||||
// been made, and the internal HTTP handler might be waiting to process it.
|
||||
// Thus, we process any pending events now, to avoid that the request is
|
||||
// processed during the tests that follow, interfering with them.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
yield promiseExecuteSoon();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@ -498,26 +564,18 @@ add_task(function test_cancel_midway_restart()
|
||||
return;
|
||||
}
|
||||
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
// The first time, cancel the download midway.
|
||||
let deferResponse = deferNextResponse();
|
||||
try {
|
||||
let deferCancel = Promise.defer();
|
||||
download.onchange = function () {
|
||||
if (!download.stopped && !download.canceled && download.progress == 50) {
|
||||
deferCancel.resolve(download.cancel());
|
||||
}
|
||||
};
|
||||
download.start();
|
||||
yield deferCancel.promise;
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
}
|
||||
yield promiseDownloadMidway(download);
|
||||
yield download.cancel();
|
||||
|
||||
do_check_true(download.stopped);
|
||||
|
||||
// The second time, we'll provide the entire interruptible response.
|
||||
continueResponses();
|
||||
download.onchange = null;
|
||||
let promiseAttempt = download.start();
|
||||
|
||||
@ -544,6 +602,115 @@ add_task(function test_cancel_midway_restart()
|
||||
TEST_DATA_SHORT + TEST_DATA_SHORT);
|
||||
});
|
||||
|
||||
/**
|
||||
* Cancels a download and restarts it from where it stopped.
|
||||
*/
|
||||
add_task(function test_cancel_midway_restart_tryToKeepPartialData()
|
||||
{
|
||||
let download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.cancel();
|
||||
|
||||
do_check_true(download.stopped);
|
||||
do_check_true(download.hasPartialData);
|
||||
|
||||
// The target file should not exist, but we should have kept the partial data.
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
|
||||
|
||||
// Verify that the server sent the response from the start.
|
||||
do_check_eq(gMostRecentFirstBytePos, 0);
|
||||
|
||||
// The second time, we'll request and obtain the second part of the response.
|
||||
continueResponses();
|
||||
yield download.start();
|
||||
|
||||
// Check that the server now sent the second part only.
|
||||
do_check_eq(gMostRecentFirstBytePos, TEST_DATA_SHORT.length);
|
||||
|
||||
// The target file should now have been created, and the ".part" file deleted.
|
||||
yield promiseVerifyContents(download.target.path,
|
||||
TEST_DATA_SHORT + TEST_DATA_SHORT);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
});
|
||||
|
||||
/**
|
||||
* Cancels a download while keeping partially downloaded data, then removes the
|
||||
* data and restarts the download from the beginning.
|
||||
*/
|
||||
add_task(function test_cancel_midway_restart_removePartialData()
|
||||
{
|
||||
let download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.cancel();
|
||||
|
||||
do_check_true(download.hasPartialData);
|
||||
yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
|
||||
|
||||
yield download.removePartialData();
|
||||
|
||||
do_check_false(download.hasPartialData);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
// The second time, we'll request and obtain the entire response again.
|
||||
continueResponses();
|
||||
yield download.start();
|
||||
|
||||
// Verify that the server sent the response from the start.
|
||||
do_check_eq(gMostRecentFirstBytePos, 0);
|
||||
|
||||
// The target file should now have been created, and the ".part" file deleted.
|
||||
yield promiseVerifyContents(download.target.path,
|
||||
TEST_DATA_SHORT + TEST_DATA_SHORT);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
});
|
||||
|
||||
/**
|
||||
* Cancels a download while keeping partially downloaded data, then removes the
|
||||
* data and restarts the download from the beginning without keeping the partial
|
||||
* data anymore.
|
||||
*/
|
||||
add_task(function test_cancel_midway_restart_tryToKeepPartialData_false()
|
||||
{
|
||||
let download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.cancel();
|
||||
|
||||
download.tryToKeepPartialData = false;
|
||||
|
||||
// The above property change does not affect existing partial data.
|
||||
do_check_true(download.hasPartialData);
|
||||
yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
|
||||
|
||||
yield download.removePartialData();
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
// Restart the download from the beginning.
|
||||
mustInterruptResponses();
|
||||
download.start();
|
||||
|
||||
yield promiseDownloadMidway(download);
|
||||
|
||||
// While the download is in progress, we should still have a ".part" file.
|
||||
do_check_false(download.hasPartialData);
|
||||
do_check_true(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
yield download.cancel();
|
||||
|
||||
// The ".part" file should be deleted now that the download is canceled.
|
||||
do_check_false(download.hasPartialData);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
// The third time, we'll request and obtain the entire response again.
|
||||
continueResponses();
|
||||
yield download.start();
|
||||
|
||||
// Verify that the server sent the response from the start.
|
||||
do_check_eq(gMostRecentFirstBytePos, 0);
|
||||
|
||||
// The target file should now have been created, and the ".part" file deleted.
|
||||
yield promiseVerifyContents(download.target.path,
|
||||
TEST_DATA_SHORT + TEST_DATA_SHORT);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
});
|
||||
|
||||
/**
|
||||
* Cancels a download right after starting it, then restarts it immediately.
|
||||
*/
|
||||
@ -554,12 +721,11 @@ add_task(function test_cancel_immediately_restart_immediately()
|
||||
return;
|
||||
}
|
||||
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
// Ensure that the download cannot complete before cancel is called.
|
||||
let deferResponse = deferNextResponse();
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
let promiseAttempt = download.start();
|
||||
|
||||
do_check_false(download.stopped);
|
||||
|
||||
download.cancel();
|
||||
@ -578,18 +744,9 @@ add_task(function test_cancel_immediately_restart_immediately()
|
||||
do_check_eq(download.totalBytes, 0);
|
||||
do_check_eq(download.currentBytes, 0);
|
||||
|
||||
// Even if we canceled the download immediately, the HTTP request might have
|
||||
// been made, and the internal HTTP handler might be waiting to process it.
|
||||
// Thus, we process any pending events now, to avoid that the request is
|
||||
// processed during the tests that follow, interfering with them.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
yield promiseExecuteSoon();
|
||||
}
|
||||
|
||||
// Ensure the next request is now allowed to complete, regardless of whether
|
||||
// the canceled request was received by the server or not.
|
||||
deferResponse.resolve();
|
||||
|
||||
continueResponses();
|
||||
try {
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
@ -619,21 +776,13 @@ add_task(function test_cancel_midway_restart_immediately()
|
||||
return;
|
||||
}
|
||||
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
let promiseAttempt = download.start();
|
||||
|
||||
// The first time, cancel the download midway.
|
||||
let deferResponse = deferNextResponse();
|
||||
|
||||
let deferMidway = Promise.defer();
|
||||
download.onchange = function () {
|
||||
if (!download.stopped && !download.canceled && download.progress == 50) {
|
||||
do_check_eq(download.progress, 50);
|
||||
deferMidway.resolve();
|
||||
}
|
||||
};
|
||||
let promiseAttempt = download.start();
|
||||
yield deferMidway.promise;
|
||||
|
||||
yield promiseDownloadMidway(download);
|
||||
download.cancel();
|
||||
do_check_true(download.canceled);
|
||||
|
||||
@ -650,9 +799,8 @@ add_task(function test_cancel_midway_restart_immediately()
|
||||
do_check_eq(download.totalBytes, 0);
|
||||
do_check_eq(download.currentBytes, 0);
|
||||
|
||||
deferResponse.resolve();
|
||||
|
||||
// The second request is allowed to complete.
|
||||
continueResponses();
|
||||
try {
|
||||
yield promiseAttempt;
|
||||
do_throw("The download should have been canceled.");
|
||||
@ -696,9 +844,8 @@ add_task(function test_cancel_successful()
|
||||
*/
|
||||
add_task(function test_cancel_twice()
|
||||
{
|
||||
// Ensure that the download cannot complete before cancel is called.
|
||||
let deferResponse = deferNextResponse();
|
||||
try {
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let promiseAttempt = download.start();
|
||||
@ -726,9 +873,55 @@ add_task(function test_cancel_twice()
|
||||
do_check_true(download.error === null);
|
||||
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that a download cannot be restarted after the "finalize" method.
|
||||
*/
|
||||
add_task(function test_finalize()
|
||||
{
|
||||
mustInterruptResponses();
|
||||
|
||||
let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
let promiseFinalized = download.finalize();
|
||||
|
||||
try {
|
||||
yield download.start();
|
||||
do_throw("It should not be possible to restart after finalization.");
|
||||
} catch (ex) { }
|
||||
|
||||
yield promiseFinalized;
|
||||
|
||||
do_check_true(download.stopped);
|
||||
do_check_false(download.succeeded);
|
||||
do_check_true(download.canceled);
|
||||
do_check_true(download.error === null);
|
||||
|
||||
do_check_false(yield OS.File.exists(download.target.path));
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that the "finalize" method can remove partially downloaded data.
|
||||
*/
|
||||
add_task(function test_finalize_tryToKeepPartialData()
|
||||
{
|
||||
// Check finalization without removing partial data.
|
||||
let download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.finalize();
|
||||
|
||||
do_check_true(download.hasPartialData);
|
||||
do_check_true(yield OS.File.exists(download.target.partFilePath));
|
||||
|
||||
// Clean up.
|
||||
yield download.removePartialData();
|
||||
|
||||
// Check finalization while removing partial data.
|
||||
download = yield promiseStartDownload_tryToKeepPartialData();
|
||||
yield download.finalize(true);
|
||||
|
||||
do_check_false(download.hasPartialData);
|
||||
do_check_false(yield OS.File.exists(download.target.partFilePath));
|
||||
});
|
||||
|
||||
/**
|
||||
@ -741,10 +934,9 @@ add_task(function test_whenSucceeded_after_restart()
|
||||
return;
|
||||
}
|
||||
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
mustInterruptResponses();
|
||||
|
||||
// Ensure that the download cannot complete before cancel is called.
|
||||
let deferResponse = deferNextResponse();
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
|
||||
|
||||
// Get a reference before the first download attempt.
|
||||
let promiseSucceeded = download.whenSucceeded();
|
||||
@ -753,9 +945,8 @@ add_task(function test_whenSucceeded_after_restart()
|
||||
download.start();
|
||||
yield download.cancel();
|
||||
|
||||
deferResponse.resolve();
|
||||
|
||||
// The second request is allowed to complete.
|
||||
continueResponses();
|
||||
download.start();
|
||||
|
||||
// Wait for the download to finish by waiting on the whenSucceeded promise.
|
||||
@ -1039,8 +1230,7 @@ add_task(function test_cancel_midway_restart_with_content_encoding()
|
||||
let download = yield promiseNewDownload(httpUrl("interruptible_gzip.txt"));
|
||||
|
||||
// The first time, cancel the download midway.
|
||||
let deferResponse = deferNextResponse();
|
||||
try {
|
||||
mustInterruptResponses();
|
||||
let deferCancel = Promise.defer();
|
||||
download.onchange = function () {
|
||||
if (!download.stopped && !download.canceled &&
|
||||
@ -1050,15 +1240,13 @@ add_task(function test_cancel_midway_restart_with_content_encoding()
|
||||
};
|
||||
download.start();
|
||||
yield deferCancel.promise;
|
||||
} finally {
|
||||
deferResponse.resolve();
|
||||
}
|
||||
|
||||
do_check_true(download.stopped);
|
||||
|
||||
// The second time, we'll provide the entire interruptible response.
|
||||
continueResponses();
|
||||
download.onchange = null;
|
||||
yield download.start()
|
||||
yield download.start();
|
||||
|
||||
do_check_eq(download.progress, 100);
|
||||
do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
|
||||
|
@ -388,55 +388,47 @@ function startFakeServer()
|
||||
}
|
||||
|
||||
/**
|
||||
* This function allows testing events or actions that need to happen in the
|
||||
* middle of a download.
|
||||
* This is an internal reference that should not be used directly by tests.
|
||||
*/
|
||||
let _gDeferResponses = Promise.defer();
|
||||
|
||||
/**
|
||||
* Ensures that all the interruptible requests started after this function is
|
||||
* called won't complete until the continueResponses function is called.
|
||||
*
|
||||
* Normally, the internal HTTP server returns all the available data as soon as
|
||||
* a request is received. In order for some requests to be served one part at a
|
||||
* time, special interruptible handlers are registered on the HTTP server.
|
||||
*
|
||||
* Before making a request to one of the addresses served by the interruptible
|
||||
* handlers, you may call "deferNextResponse" to get a reference to an object
|
||||
* that allows you to control the next request.
|
||||
* time, special interruptible handlers are registered on the HTTP server. This
|
||||
* allows testing events or actions that need to happen in the middle of a
|
||||
* download.
|
||||
*
|
||||
* For example, the handler accessible at the httpUri("interruptible.txt")
|
||||
* address 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.
|
||||
* address returns the TEST_DATA_SHORT text, then it may block until the
|
||||
* continueResponses method is called. 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.
|
||||
*
|
||||
* @returns Deferred object used to control the response.
|
||||
* If an interruptible request is started before the function is called, it may
|
||||
* or may not be blocked depending on the actual sequence of events.
|
||||
*/
|
||||
function deferNextResponse()
|
||||
function mustInterruptResponses()
|
||||
{
|
||||
do_print("Interruptible request will be controlled.");
|
||||
// If there are pending blocked requests, allow them to complete. This is
|
||||
// done to prevent requests from being blocked forever, but should not affect
|
||||
// the test logic, since previously started requests should not be monitored
|
||||
// on the client side anymore.
|
||||
_gDeferResponses.resolve();
|
||||
|
||||
// Store an internal reference that should not be used directly by tests.
|
||||
if (!deferNextResponse._deferred) {
|
||||
deferNextResponse._deferred = Promise.defer();
|
||||
}
|
||||
return deferNextResponse._deferred;
|
||||
do_print("Interruptible responses will be blocked midway.");
|
||||
_gDeferResponses = Promise.defer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that is resolved when the next interruptible response
|
||||
* handler has received the request, and has started sending the first part of
|
||||
* the response. The response might not have been received by the client yet.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the next request has been received.
|
||||
* @rejects Never.
|
||||
* Allows all the current and future interruptible requests to complete.
|
||||
*/
|
||||
function promiseNextRequestReceived()
|
||||
function continueResponses()
|
||||
{
|
||||
do_print("Requested notification when interruptible request is received.");
|
||||
|
||||
// Store an internal reference that should not be used directly by tests.
|
||||
promiseNextRequestReceived._deferred = Promise.defer();
|
||||
return promiseNextRequestReceived._deferred.promise;
|
||||
do_print("Interruptible responses are now allowed to continue.");
|
||||
_gDeferResponses.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -448,44 +440,24 @@ function promiseNextRequestReceived()
|
||||
* This function is called when the response is received, with the
|
||||
* aRequest and aResponse arguments of the server.
|
||||
* @param aSecondPartFn
|
||||
* This function is called after the "resolve" method of the object
|
||||
* returned by deferNextResponse is called. This function is called with
|
||||
* the aRequest and aResponse arguments of the server.
|
||||
* This function is called with the aRequest and aResponse arguments of
|
||||
* the server, when the continueResponses function is called.
|
||||
*/
|
||||
function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn)
|
||||
{
|
||||
gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) {
|
||||
// Get a reference to the controlling object for this request. If the
|
||||
// deferNextResponse function was not called, interrupt the test.
|
||||
let deferResponse = deferNextResponse._deferred;
|
||||
deferNextResponse._deferred = null;
|
||||
if (deferResponse) {
|
||||
do_print("Interruptible request started under control.");
|
||||
} else {
|
||||
do_print("Interruptible request started without being controlled.");
|
||||
deferResponse = Promise.defer();
|
||||
deferResponse.resolve();
|
||||
}
|
||||
do_print("Interruptible request started.");
|
||||
|
||||
// Process the first part of the response.
|
||||
aResponse.processAsync();
|
||||
aFirstPartFn(aRequest, aResponse);
|
||||
|
||||
if (promiseNextRequestReceived._deferred) {
|
||||
do_print("Notifying that interruptible request has been received.");
|
||||
promiseNextRequestReceived._deferred.resolve();
|
||||
promiseNextRequestReceived._deferred = null;
|
||||
}
|
||||
|
||||
// Wait on the deferred object, then finish or abort the request.
|
||||
deferResponse.promise.then(function RIH_onSuccess() {
|
||||
// Wait on the current deferred object, then finish the request.
|
||||
_gDeferResponses.promise.then(function RIH_onSuccess() {
|
||||
aSecondPartFn(aRequest, aResponse);
|
||||
aResponse.finish();
|
||||
do_print("Interruptible request finished.");
|
||||
}, function RIH_onFailure() {
|
||||
aResponse.abort();
|
||||
do_print("Interruptible request aborted.");
|
||||
});
|
||||
}).then(null, Cu.reportError);
|
||||
});
|
||||
}
|
||||
|
||||
@ -499,6 +471,12 @@ function isValidDate(aDate) {
|
||||
return aDate && aDate.getTime && !isNaN(aDate.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Position of the first byte served by the "interruptible_resumable.txt"
|
||||
* handler during the most recent response.
|
||||
*/
|
||||
let gMostRecentFirstBytePos;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Initialization functions common to all tests
|
||||
|
||||
@ -519,11 +497,48 @@ add_task(function test_common_initialize()
|
||||
aResponse.write(TEST_DATA_SHORT);
|
||||
});
|
||||
|
||||
registerInterruptibleHandler("/empty-noprogress.txt",
|
||||
registerInterruptibleHandler("/interruptible_resumable.txt",
|
||||
function firstPart(aRequest, aResponse) {
|
||||
aResponse.setHeader("Content-Type", "text/plain", false);
|
||||
}, function secondPart(aRequest, aResponse) { });
|
||||
|
||||
// Determine if only part of the data should be sent.
|
||||
let data = TEST_DATA_SHORT + TEST_DATA_SHORT;
|
||||
if (aRequest.hasHeader("Range")) {
|
||||
var matches = aRequest.getHeader("Range")
|
||||
.match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
|
||||
var firstBytePos = (matches[1] === undefined) ? 0 : matches[1];
|
||||
var lastBytePos = (matches[2] === undefined) ? data.length - 1
|
||||
: matches[2];
|
||||
if (firstBytePos >= data.length) {
|
||||
aResponse.setStatusLine(aRequest.httpVersion, 416,
|
||||
"Requested Range Not Satisfiable");
|
||||
aResponse.setHeader("Content-Range", "*/" + data.length, false);
|
||||
aResponse.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
aResponse.setStatusLine(aRequest.httpVersion, 206, "Partial Content");
|
||||
aResponse.setHeader("Content-Range", firstBytePos + "-" +
|
||||
lastBytePos + "/" +
|
||||
data.length, false);
|
||||
|
||||
data = data.substring(firstBytePos, lastBytePos + 1);
|
||||
|
||||
gMostRecentFirstBytePos = firstBytePos;
|
||||
} else {
|
||||
gMostRecentFirstBytePos = 0;
|
||||
}
|
||||
|
||||
aResponse.setHeader("Content-Length", "" + data.length, false);
|
||||
|
||||
aResponse.write(data.substring(0, data.length / 2));
|
||||
|
||||
// Store the second part of the data on the response object, so that it
|
||||
// can be used by the secondPart function.
|
||||
aResponse.secondPartData = data.substring(data.length / 2);
|
||||
}, function secondPart(aRequest, aResponse) {
|
||||
aResponse.write(aResponse.secondPartData);
|
||||
});
|
||||
|
||||
registerInterruptibleHandler("/interruptible_gzip.txt",
|
||||
function firstPart(aRequest, aResponse) {
|
||||
|
Loading…
Reference in New Issue
Block a user