зеркало из https://github.com/mozilla/gecko-dev.git
Bug 836443 - Automatically stop and restart downloads. r=enn
This commit is contained in:
Родитель
9d8c642be5
Коммит
9981fae81f
|
@ -203,7 +203,7 @@ Download.prototype = {
|
|||
*
|
||||
* 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.
|
||||
* successfully, this property is always 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.
|
||||
|
@ -382,6 +382,7 @@ Download.prototype = {
|
|||
// Update the status properties for a successful download.
|
||||
this.progress = 100;
|
||||
this.succeeded = true;
|
||||
this.hasPartialData = false;
|
||||
} catch (ex) {
|
||||
// Fail with a generic status code on cancellation, so that the caller
|
||||
// is forced to actually check the status properties to see if the
|
||||
|
@ -622,6 +623,47 @@ Download.prototype = {
|
|||
return this._deferSucceeded.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the state of a finished, failed, or canceled download based on the
|
||||
* current state in the file system. If the download is in progress or it has
|
||||
* been finalized, this method has no effect, and it returns a resolved
|
||||
* promise.
|
||||
*
|
||||
* This allows the properties of the download to be updated in case the user
|
||||
* moved or deleted the target file or its associated ".part" file.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the operation has completed.
|
||||
* @rejects Never.
|
||||
*/
|
||||
refresh: function ()
|
||||
{
|
||||
return Task.spawn(function () {
|
||||
if (!this.stopped || this._finalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the current progress from disk if we retained partial data.
|
||||
if (this.hasPartialData && this.target.partFilePath) {
|
||||
let stat = yield OS.File.stat(this.target.partFilePath);
|
||||
|
||||
// Ignore the result if the state has changed meanwhile.
|
||||
if (!this.stopped || this._finalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the bytes transferred and the related progress properties.
|
||||
this.currentBytes = stat.size;
|
||||
if (this.totalBytes > 0) {
|
||||
this.hasProgress = true;
|
||||
this.progress = Math.floor(this.currentBytes /
|
||||
this.totalBytes * 100);
|
||||
}
|
||||
this._notifyChange();
|
||||
}
|
||||
}.bind(this)).then(null, Cu.reportError);
|
||||
},
|
||||
|
||||
/**
|
||||
* True if the "finalize" method has been called. This prevents the download
|
||||
* from starting again after having been stopped.
|
||||
|
@ -761,20 +803,54 @@ Download.prototype = {
|
|||
serializable.saver = saver;
|
||||
}
|
||||
|
||||
if (this.launcherPath) {
|
||||
serializable.launcherPath = this.launcherPath;
|
||||
if (!this.stopped) {
|
||||
serializable.stopped = false;
|
||||
}
|
||||
|
||||
if (this.launchWhenSucceeded) {
|
||||
serializable.launchWhenSucceeded = true;
|
||||
if (this.error && ("message" in this.error)) {
|
||||
serializable.error = { message: this.error.message };
|
||||
}
|
||||
|
||||
if (this.contentType) {
|
||||
serializable.contentType = this.contentType;
|
||||
// These are serialized unless they are false, null, or empty strings.
|
||||
let propertiesToSerialize = [
|
||||
"succeeded",
|
||||
"canceled",
|
||||
"startTime",
|
||||
"totalBytes",
|
||||
"hasPartialData",
|
||||
"tryToKeepPartialData",
|
||||
"launcherPath",
|
||||
"launchWhenSucceeded",
|
||||
"contentType",
|
||||
];
|
||||
|
||||
for (let property of propertiesToSerialize) {
|
||||
if (this[property]) {
|
||||
serializable[property] = this[property];
|
||||
}
|
||||
}
|
||||
|
||||
return serializable;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a value that changes only when one of the properties of a Download
|
||||
* object that should be saved into a file also change. This excludes
|
||||
* properties whose value doesn't usually change during the download lifetime.
|
||||
*
|
||||
* This function is used to determine whether the download should be
|
||||
* serialized after a property change notification has been received.
|
||||
*
|
||||
* @return String representing the relevant download state.
|
||||
*/
|
||||
getSerializationHash: function ()
|
||||
{
|
||||
// The "succeeded", "canceled", "error", and startTime properties are not
|
||||
// taken into account because they all change before the "stopped" property
|
||||
// changes, and are not altered in other cases.
|
||||
return this.stopped + "," + this.totalBytes + "," + this.hasPartialData +
|
||||
"," + this.contentType;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -816,16 +892,28 @@ Download.fromSerializable = function (aSerializable) {
|
|||
}
|
||||
download.saver.download = download;
|
||||
|
||||
if ("launchWhenSucceeded" in aSerializable) {
|
||||
download.launchWhenSucceeded = !!aSerializable.launchWhenSucceeded;
|
||||
let propertiesToDeserialize = [
|
||||
"startTime",
|
||||
"totalBytes",
|
||||
"hasPartialData",
|
||||
"tryToKeepPartialData",
|
||||
"launcherPath",
|
||||
"launchWhenSucceeded",
|
||||
"contentType",
|
||||
];
|
||||
|
||||
// If the download should not be restarted automatically, update its state to
|
||||
// reflect success or failure during a previous session.
|
||||
if (!("stopped" in aSerializable) || aSerializable.stopped) {
|
||||
propertiesToDeserialize.push("succeeded");
|
||||
propertiesToDeserialize.push("canceled");
|
||||
propertiesToDeserialize.push("error");
|
||||
}
|
||||
|
||||
if ("contentType" in aSerializable) {
|
||||
download.contentType = aSerializable.contentType;
|
||||
for (let property of propertiesToDeserialize) {
|
||||
if (property in aSerializable) {
|
||||
download[property] = aSerializable[property];
|
||||
}
|
||||
|
||||
if ("launcherPath" in aSerializable) {
|
||||
download.launcherPath = aSerializable.launcherPath;
|
||||
}
|
||||
|
||||
return download;
|
||||
|
@ -1190,6 +1278,13 @@ DownloadCopySaver.prototype = {
|
|||
*/
|
||||
_canceled: false,
|
||||
|
||||
/**
|
||||
* String corresponding to the entityID property of the nsIResumableChannel
|
||||
* used to execute the download, or null if the channel was not resumable or
|
||||
* the saver was instructed not to keep partially downloaded data.
|
||||
*/
|
||||
entityID: null,
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.execute".
|
||||
*/
|
||||
|
@ -1428,8 +1523,13 @@ DownloadCopySaver.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.entityID) {
|
||||
return "copy";
|
||||
}
|
||||
|
||||
return { type: "copy",
|
||||
entityID: this.entityID };
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1443,8 +1543,11 @@ DownloadCopySaver.prototype = {
|
|||
* @return The newly created DownloadCopySaver object.
|
||||
*/
|
||||
DownloadCopySaver.fromSerializable = function (aSerializable) {
|
||||
// We don't have other state details for now.
|
||||
return new DownloadCopySaver();
|
||||
let saver = new DownloadCopySaver();
|
||||
if ("entityID" in aSerializable) {
|
||||
saver.entityID = aSerializable.entityID;
|
||||
}
|
||||
return saver;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -1574,6 +1677,13 @@ DownloadLegacySaver.prototype = {
|
|||
*/
|
||||
copySaver: null,
|
||||
|
||||
/**
|
||||
* String corresponding to the entityID property of the nsIResumableChannel
|
||||
* used to execute the download, or null if the channel was not resumable or
|
||||
* the saver was instructed not to keep partially downloaded data.
|
||||
*/
|
||||
entityID: null,
|
||||
|
||||
/**
|
||||
* Implements "DownloadSaver.execute".
|
||||
*/
|
||||
|
@ -1674,7 +1784,7 @@ DownloadLegacySaver.prototype = {
|
|||
// thus it cannot be rebuilt during deserialization. To support resuming
|
||||
// across different browser sessions, this object is transformed into a
|
||||
// DownloadCopySaver for the purpose of serialization.
|
||||
return "copy";
|
||||
return DownloadCopySaver.prototype.toSerializable.call(this);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -64,6 +64,31 @@ XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
|
|||
createBundle("chrome://mozapps/locale/downloads/downloads.properties");
|
||||
});
|
||||
|
||||
const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
|
||||
"initWithCallback");
|
||||
|
||||
/**
|
||||
* Indicates the delay between a change to the downloads data and the related
|
||||
* save operation. This value is the result of a delicate trade-off, assuming
|
||||
* the host application uses the browser history instead of the download store
|
||||
* to save completed downloads.
|
||||
*
|
||||
* If a download takes less than this interval to complete (for example, saving
|
||||
* a page that is already displayed), then no input/output is triggered by the
|
||||
* download store except for an existence check, resulting in the best possible
|
||||
* efficiency.
|
||||
*
|
||||
* Conversely, if the browser is closed before this interval has passed, the
|
||||
* download will not be saved. This prevents it from being restored in the next
|
||||
* session, and if there is partial data associated with it, then the ".part"
|
||||
* file will not be deleted when the browser starts again.
|
||||
*
|
||||
* In all cases, for best efficiency, this value should be high enough that the
|
||||
* input/output for opening or closing the target file does not overlap with the
|
||||
* one for saving the list of downloads.
|
||||
*/
|
||||
const kSaveDelayMs = 1500;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// DownloadIntegration
|
||||
|
||||
|
@ -105,7 +130,7 @@ this.DownloadIntegration = {
|
|||
* @param aList
|
||||
* DownloadList object to be populated with the download objects
|
||||
* serialized from the previous session. This list will be persisted
|
||||
* to disk during the session lifetime or when the session terminates.
|
||||
* to disk during the session lifetime.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When the list has been populated.
|
||||
|
@ -124,7 +149,39 @@ this.DownloadIntegration = {
|
|||
this._store = new DownloadStore(aList, OS.Path.join(
|
||||
OS.Constants.Path.profileDir,
|
||||
"downloads.json"));
|
||||
return this._store.load();
|
||||
this._store.onsaveitem = this.shouldPersistDownload.bind(this);
|
||||
|
||||
// Load the list of persistent downloads, then add the DownloadAutoSaveView
|
||||
// even if the load operation failed.
|
||||
return this._store.load().then(null, Cu.reportError).then(() => {
|
||||
new DownloadAutoSaveView(aList, this._store);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if a Download object from the list of persistent downloads
|
||||
* should be saved into a file, so that it can be restored across sessions.
|
||||
*
|
||||
* This function allows filtering out downloads that the host application is
|
||||
* not interested in persisting across sessions, for example downloads that
|
||||
* finished successfully.
|
||||
*
|
||||
* @param aDownload
|
||||
* The Download object to be inspected. This is originally taken from
|
||||
* the global DownloadList object for downloads that were not started
|
||||
* from a private browsing window. The item may have been removed
|
||||
* from the list since the save operation started, though in this case
|
||||
* the save operation will be repeated later.
|
||||
*
|
||||
* @return True to save the download, false otherwise.
|
||||
*/
|
||||
shouldPersistDownload: function (aDownload)
|
||||
{
|
||||
// In the default implementation, we save all the downloads currently in
|
||||
// progress, as well as stopped downloads for which we retained partially
|
||||
// downloaded data. Stopped downloads for which we don't need to track the
|
||||
// presence of a ".part" file are only retained in the browser history.
|
||||
return aDownload.hasPartialData || !aDownload.stopped;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -493,6 +550,10 @@ this.DownloadIntegration = {
|
|||
* @resolves When the views and observers are added.
|
||||
*/
|
||||
addListObservers: function DI_addListObservers(aList, aIsPrivate) {
|
||||
if (this.dontLoad) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
DownloadObserver.registerView(aList, aIsPrivate);
|
||||
if (!DownloadObserver.observersAdded) {
|
||||
DownloadObserver.observersAdded = true;
|
||||
|
@ -504,7 +565,10 @@ this.DownloadIntegration = {
|
|||
}
|
||||
};
|
||||
|
||||
let DownloadObserver = {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// DownloadObserver
|
||||
|
||||
this.DownloadObserver = {
|
||||
/**
|
||||
* Flag to determine if the observers have been added previously.
|
||||
*/
|
||||
|
@ -657,3 +721,134 @@ let DownloadObserver = {
|
|||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference])
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// DownloadAutoSaveView
|
||||
|
||||
/**
|
||||
* This view can be added to a DownloadList object to trigger a save operation
|
||||
* in the given DownloadStore object when a relevant change occurs.
|
||||
*
|
||||
* @param aStore
|
||||
* The DownloadStore object used for saving.
|
||||
*/
|
||||
function DownloadAutoSaveView(aList, aStore) {
|
||||
this._store = aStore;
|
||||
this._downloadsMap = new Map();
|
||||
|
||||
// We set _initialized to true after adding the view, so that onDownloadAdded
|
||||
// doesn't cause a save to occur.
|
||||
aList.addView(this);
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
DownloadAutoSaveView.prototype = {
|
||||
/**
|
||||
* True when the initial state of the downloads has been loaded.
|
||||
*/
|
||||
_initialized: false,
|
||||
|
||||
/**
|
||||
* The DownloadStore object used for saving.
|
||||
*/
|
||||
_store: null,
|
||||
|
||||
/**
|
||||
* This map contains only Download objects that should be saved to disk, and
|
||||
* associates them with the result of their getSerializationHash function, for
|
||||
* the purpose of detecting changes to the relevant properties.
|
||||
*/
|
||||
_downloadsMap: null,
|
||||
|
||||
/**
|
||||
* This is set to true when the save operation should be triggered. This is
|
||||
* required so that a new operation can be scheduled while the current one is
|
||||
* in progress, without re-entering the save method.
|
||||
*/
|
||||
_shouldSave: false,
|
||||
|
||||
/**
|
||||
* nsITimer used for triggering the save operation after a delay, or null if
|
||||
* saving has finished and there is no operation scheduled for execution.
|
||||
*
|
||||
* The logic here is different from the DeferredTask module in that multiple
|
||||
* requests will never delay the operation for longer than the expected time
|
||||
* (no grace delay), and the operation is never re-entered during execution.
|
||||
*/
|
||||
_timer: null,
|
||||
|
||||
/**
|
||||
* Timer callback used to serialize the list of downloads.
|
||||
*/
|
||||
_save: function ()
|
||||
{
|
||||
Task.spawn(function () {
|
||||
// Any save request received during execution will be handled later.
|
||||
this._shouldSave = false;
|
||||
|
||||
// Execute the asynchronous save operation.
|
||||
try {
|
||||
yield this._store.save();
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
|
||||
// Handle requests received during the operation.
|
||||
this._timer = null;
|
||||
if (this._shouldSave) {
|
||||
this.saveSoon();
|
||||
}
|
||||
}.bind(this)).then(null, Cu.reportError);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the list of downloads changed, this triggers the asynchronous
|
||||
* serialization of the list of downloads.
|
||||
*/
|
||||
saveSoon: function ()
|
||||
{
|
||||
this._shouldSave = true;
|
||||
if (!this._timer) {
|
||||
this._timer = new Timer(this._save.bind(this), kSaveDelayMs,
|
||||
Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
}
|
||||
},
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//// DownloadList view
|
||||
|
||||
onDownloadAdded: function (aDownload)
|
||||
{
|
||||
if (DownloadIntegration.shouldPersistDownload(aDownload)) {
|
||||
this._downloadsMap.set(aDownload, aDownload.getSerializationHash());
|
||||
if (this._initialized) {
|
||||
this.saveSoon();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onDownloadChanged: function (aDownload)
|
||||
{
|
||||
if (!DownloadIntegration.shouldPersistDownload(aDownload)) {
|
||||
if (this._downloadsMap.has(aDownload)) {
|
||||
this._downloadsMap.delete(aDownload);
|
||||
this.saveSoon();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let hash = aDownload.getSerializationHash();
|
||||
if (this._downloadsMap.get(aDownload) != hash) {
|
||||
this._downloadsMap.set(aDownload, hash);
|
||||
this.saveSoon();
|
||||
}
|
||||
},
|
||||
|
||||
onDownloadRemoved: function (aDownload)
|
||||
{
|
||||
if (this._downloadsMap.has(aDownload)) {
|
||||
this._downloadsMap.delete(aDownload);
|
||||
this.saveSoon();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -171,6 +171,11 @@ DownloadList.prototype = {
|
|||
* // Called after aDownload is removed from the list.
|
||||
* },
|
||||
* }
|
||||
*
|
||||
* @note The onDownloadAdded notifications are sent synchronously. This
|
||||
* allows for a complete initialization of the view used for detecting
|
||||
* changes to downloads to be persisted, before other callers get a
|
||||
* chance to modify them.
|
||||
*/
|
||||
addView: function DL_addView(aView)
|
||||
{
|
||||
|
|
|
@ -88,6 +88,12 @@ DownloadStore.prototype = {
|
|||
*/
|
||||
path: "",
|
||||
|
||||
/**
|
||||
* This function is called with a Download object as its first argument, and
|
||||
* should return true if the item should be saved.
|
||||
*/
|
||||
onsaveitem: () => true,
|
||||
|
||||
/**
|
||||
* Loads persistent downloads from the file to the list.
|
||||
*
|
||||
|
@ -111,7 +117,23 @@ DownloadStore.prototype = {
|
|||
// Create live downloads based on the static snapshot.
|
||||
for (let downloadData of storeData.list) {
|
||||
try {
|
||||
this.list.add(yield Downloads.createDownload(downloadData));
|
||||
let download = yield Downloads.createDownload(downloadData);
|
||||
try {
|
||||
if (("stopped" in downloadData) && !downloadData.stopped) {
|
||||
// Try to restart the download if it was in progress during the
|
||||
// previous session.
|
||||
download.start();
|
||||
} else {
|
||||
// If the download was not in progress, try to update the current
|
||||
// progress from disk. This is relevant in case we retained
|
||||
// partially downloaded data.
|
||||
yield download.refresh();
|
||||
}
|
||||
} finally {
|
||||
// Add the download to the list if we succeeded in creating it,
|
||||
// after we have updated its initial state.
|
||||
this.list.add(download);
|
||||
}
|
||||
} catch (ex) {
|
||||
// If an item is unrecognized, don't prevent others from being loaded.
|
||||
Cu.reportError(ex);
|
||||
|
@ -139,6 +161,9 @@ DownloadStore.prototype = {
|
|||
let atLeastOneDownload = false;
|
||||
for (let download of downloads) {
|
||||
try {
|
||||
if (!this.onsaveitem(download)) {
|
||||
continue;
|
||||
}
|
||||
storeData.list.push(download.toSerializable());
|
||||
atLeastOneDownload = true;
|
||||
} catch (ex) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче