653 lines
17 KiB
JavaScript
653 lines
17 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/**
|
|
* Provides collections of Download objects and aggregate views on them.
|
|
*/
|
|
|
|
const FILE_EXTENSIONS = [
|
|
"aac",
|
|
"adt",
|
|
"adts",
|
|
"accdb",
|
|
"accde",
|
|
"accdr",
|
|
"accdt",
|
|
"aif",
|
|
"aifc",
|
|
"aiff",
|
|
"apng",
|
|
"aspx",
|
|
"avi",
|
|
"avif",
|
|
"bat",
|
|
"bin",
|
|
"bmp",
|
|
"cab",
|
|
"cda",
|
|
"csv",
|
|
"dif",
|
|
"dll",
|
|
"doc",
|
|
"docm",
|
|
"docx",
|
|
"dot",
|
|
"dotx",
|
|
"eml",
|
|
"eps",
|
|
"exe",
|
|
"flac",
|
|
"flv",
|
|
"gif",
|
|
"htm",
|
|
"html",
|
|
"ico",
|
|
"ini",
|
|
"iso",
|
|
"jar",
|
|
"jfif",
|
|
"jpg",
|
|
"jpeg",
|
|
"json",
|
|
"m4a",
|
|
"mdb",
|
|
"mid",
|
|
"midi",
|
|
"mov",
|
|
"mp3",
|
|
"mp4",
|
|
"mpeg",
|
|
"mpg",
|
|
"msi",
|
|
"mui",
|
|
"oga",
|
|
"ogg",
|
|
"ogv",
|
|
"opus",
|
|
"pdf",
|
|
"pjpeg",
|
|
"pjp",
|
|
"png",
|
|
"pot",
|
|
"potm",
|
|
"potx",
|
|
"ppam",
|
|
"pps",
|
|
"ppsm",
|
|
"ppsx",
|
|
"ppt",
|
|
"pptm",
|
|
"pptx",
|
|
"psd",
|
|
"pst",
|
|
"pub",
|
|
"rar",
|
|
"rdf",
|
|
"rtf",
|
|
"shtml",
|
|
"sldm",
|
|
"sldx",
|
|
"svg",
|
|
"swf",
|
|
"sys",
|
|
"tif",
|
|
"tiff",
|
|
"tmp",
|
|
"txt",
|
|
"vob",
|
|
"vsd",
|
|
"vsdm",
|
|
"vsdx",
|
|
"vss",
|
|
"vssm",
|
|
"vst",
|
|
"vstm",
|
|
"vstx",
|
|
"wav",
|
|
"wbk",
|
|
"webm",
|
|
"webp",
|
|
"wks",
|
|
"wma",
|
|
"wmd",
|
|
"wmv",
|
|
"wmz",
|
|
"wms",
|
|
"wpd",
|
|
"wp5",
|
|
"xht",
|
|
"xhtml",
|
|
"xla",
|
|
"xlam",
|
|
"xll",
|
|
"xlm",
|
|
"xls",
|
|
"xlsm",
|
|
"xlsx",
|
|
"xlt",
|
|
"xltm",
|
|
"xltx",
|
|
"xml",
|
|
"zip",
|
|
];
|
|
|
|
/**
|
|
* Represents a collection of Download objects that can be viewed and managed by
|
|
* the user interface, and persisted across sessions.
|
|
*/
|
|
export class DownloadList {
|
|
constructor() {
|
|
/**
|
|
* Array of Download objects currently in the list.
|
|
*/
|
|
this._downloads = [];
|
|
|
|
/**
|
|
* Set of currently registered views.
|
|
*/
|
|
this._views = new Set();
|
|
}
|
|
|
|
/**
|
|
* Retrieves a snapshot of the downloads that are currently in the list. The
|
|
* returned array does not change when downloads are added or removed, though
|
|
* the Download objects it contains are still updated in real time.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves An array of Download objects.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async getAll() {
|
|
return Array.from(this._downloads);
|
|
}
|
|
|
|
/**
|
|
* Adds a new download to the end of the items list.
|
|
*
|
|
* @note When a download is added to the list, its "onchange" event is
|
|
* registered by the list, thus it cannot be used to monitor the
|
|
* download. To receive change notifications for downloads that are
|
|
* added to the list, use the addView method to register for
|
|
* onDownloadChanged notifications.
|
|
*
|
|
* @param download
|
|
* The Download object to add.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the download has been added.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async add(download) {
|
|
this._downloads.push(download);
|
|
download.onchange = this._change.bind(this, download);
|
|
this._notifyAllViews("onDownloadAdded", download);
|
|
}
|
|
|
|
/**
|
|
* 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 download
|
|
* The Download object to remove.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the download has been removed.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async remove(download) {
|
|
let index = this._downloads.indexOf(download);
|
|
if (index != -1) {
|
|
this._downloads.splice(index, 1);
|
|
download.onchange = null;
|
|
this._notifyAllViews("onDownloadRemoved", download);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function is called when "onchange" events of downloads occur.
|
|
*
|
|
* @param download
|
|
* The Download object that changed.
|
|
*/
|
|
_change(download) {
|
|
this._notifyAllViews("onDownloadChanged", download);
|
|
}
|
|
|
|
/**
|
|
* Adds a view that will be notified of changes to downloads. The newly added
|
|
* view will receive onDownloadAdded notifications for all the downloads that
|
|
* are already in the list.
|
|
*
|
|
* @param view
|
|
* The view object to add. The following methods may be defined:
|
|
* {
|
|
* onDownloadAdded: function (download) {
|
|
* // Called after download is added to the end of the list.
|
|
* },
|
|
* onDownloadChanged: function (download) {
|
|
* // Called after the properties of download change.
|
|
* },
|
|
* onDownloadRemoved: function (download) {
|
|
* // Called after download is removed from the list.
|
|
* },
|
|
* onDownloadBatchStarting: function () {
|
|
* // Called before multiple changes are made at the same time.
|
|
* },
|
|
* onDownloadBatchEnded: function () {
|
|
* // Called after all the changes have been made.
|
|
* },
|
|
* }
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the view has been registered and all the onDownloadAdded
|
|
* notifications for the existing downloads have been sent.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async addView(view) {
|
|
this._views.add(view);
|
|
|
|
if ("onDownloadAdded" in view) {
|
|
this._notifyAllViews("onDownloadBatchStarting");
|
|
for (let download of this._downloads) {
|
|
try {
|
|
view.onDownloadAdded(download);
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
this._notifyAllViews("onDownloadBatchEnded");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes a view that was previously added using addView.
|
|
*
|
|
* @param view
|
|
* The view object to remove.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the view has been removed. At this point, the removed view
|
|
* will not receive any more notifications.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async removeView(view) {
|
|
this._views.delete(view);
|
|
}
|
|
|
|
/**
|
|
* Notifies all the views of a download addition, change, removal, or other
|
|
* event. The additional arguments are passed to the called method.
|
|
*
|
|
* @param methodName
|
|
* String containing the name of the method to call on the view.
|
|
*/
|
|
_notifyAllViews(methodName, ...args) {
|
|
for (let view of this._views) {
|
|
try {
|
|
if (methodName in view) {
|
|
view[methodName](...args);
|
|
}
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes downloads from the list that have finished, have failed, or have
|
|
* been canceled without keeping partial data. A filter function may be
|
|
* specified to remove only a subset of those downloads.
|
|
*
|
|
* This method finalizes each removed download, ensuring that any partially
|
|
* downloaded data associated with it is also removed.
|
|
*
|
|
* @param filterFn
|
|
* The filter function is called with each download as its only
|
|
* argument, and should return true to remove the download and false
|
|
* to keep it. This parameter may be null or omitted to have no
|
|
* additional filter.
|
|
*/
|
|
removeFinished(filterFn) {
|
|
(async () => {
|
|
let list = await this.getAll();
|
|
for (let download of list) {
|
|
// Remove downloads that have been canceled, even if the cancellation
|
|
// operation hasn't completed yet so we don't check "stopped" here.
|
|
// Failed downloads with partial data are also removed.
|
|
if (
|
|
download.stopped &&
|
|
(!download.hasPartialData || download.error) &&
|
|
(!filterFn || filterFn(download))
|
|
) {
|
|
// Remove the download first, so that the views don't get the change
|
|
// notifications that may occur during finalization.
|
|
await this.remove(download);
|
|
// Find if a file with the same path is also downloading.
|
|
let sameFileIsDownloading = false;
|
|
for (let otherDownload of await this.getAll()) {
|
|
if (
|
|
download !== otherDownload &&
|
|
download.target.path == otherDownload.target.path &&
|
|
!otherDownload.error
|
|
) {
|
|
sameFileIsDownloading = true;
|
|
}
|
|
}
|
|
// 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.
|
|
let removePartialData = !sameFileIsDownloading;
|
|
download.finalize(removePartialData).catch(console.error);
|
|
}
|
|
}
|
|
})().catch(console.error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides a unified, unordered list combining public and private downloads.
|
|
*
|
|
* Download objects added to this list are also added to one of the two
|
|
* underlying lists, based on their "source.isPrivate" property. Views on this
|
|
* list will receive notifications for both public and private downloads.
|
|
*
|
|
* @param publicList
|
|
* Underlying DownloadList containing public downloads.
|
|
* @param privateList
|
|
* Underlying DownloadList containing private downloads.
|
|
*/
|
|
export class DownloadCombinedList extends DownloadList {
|
|
constructor(publicList, privateList) {
|
|
super();
|
|
|
|
/**
|
|
* Underlying DownloadList containing public downloads.
|
|
*/
|
|
this._publicList = publicList;
|
|
|
|
/**
|
|
* Underlying DownloadList containing private downloads.
|
|
*/
|
|
this._privateList = privateList;
|
|
|
|
publicList.addView(this).catch(console.error);
|
|
privateList.addView(this).catch(console.error);
|
|
}
|
|
|
|
/**
|
|
* Adds a new download to the end of the items list.
|
|
*
|
|
* @note When a download is added to the list, its "onchange" event is
|
|
* registered by the list, thus it cannot be used to monitor the
|
|
* download. To receive change notifications for downloads that are
|
|
* added to the list, use the addView method to register for
|
|
* onDownloadChanged notifications.
|
|
*
|
|
* @param download
|
|
* The Download object to add.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the download has been added.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
add(download) {
|
|
let extension = download.target.path.split(".").pop();
|
|
|
|
if (!FILE_EXTENSIONS.includes(extension)) {
|
|
extension = "other";
|
|
}
|
|
|
|
Glean.downloads.addedFileExtension.record({ value: extension });
|
|
|
|
if (download.source.isPrivate) {
|
|
return this._privateList.add(download);
|
|
}
|
|
|
|
return this._publicList.add(download);
|
|
}
|
|
|
|
/**
|
|
* 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 download
|
|
* The Download object to remove.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the download has been removed.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
remove(download) {
|
|
if (download.source.isPrivate) {
|
|
return this._privateList.remove(download);
|
|
}
|
|
return this._publicList.remove(download);
|
|
}
|
|
|
|
// DownloadList callback
|
|
onDownloadAdded(download) {
|
|
this._downloads.push(download);
|
|
this._notifyAllViews("onDownloadAdded", download);
|
|
}
|
|
|
|
// DownloadList callback
|
|
onDownloadChanged(download) {
|
|
this._notifyAllViews("onDownloadChanged", download);
|
|
}
|
|
|
|
// DownloadList callback
|
|
onDownloadRemoved(download) {
|
|
let index = this._downloads.indexOf(download);
|
|
if (index != -1) {
|
|
this._downloads.splice(index, 1);
|
|
}
|
|
this._notifyAllViews("onDownloadRemoved", download);
|
|
}
|
|
}
|
|
/**
|
|
* Provides an aggregated view on the contents of a DownloadList.
|
|
*/
|
|
export class DownloadSummary {
|
|
constructor() {
|
|
/**
|
|
* Array of Download objects that are currently part of the summary.
|
|
*/
|
|
this._downloads = [];
|
|
|
|
/**
|
|
* Set of currently registered views.
|
|
*/
|
|
this._views = new Set();
|
|
}
|
|
|
|
/**
|
|
* Underlying DownloadList whose contents should be summarized.
|
|
*/
|
|
_list = null;
|
|
|
|
/**
|
|
* Indicates whether all the downloads are currently stopped.
|
|
*/
|
|
allHaveStopped = true;
|
|
|
|
/**
|
|
* Indicates whether whether all downloads have an unknown final size.
|
|
*/
|
|
allUnknownSize = true;
|
|
|
|
/**
|
|
* Indicates the total number of bytes to be transferred before completing all
|
|
* the downloads that are currently in progress.
|
|
*
|
|
* For downloads that do not have a known final size, the number of bytes
|
|
* currently transferred is reported as part of this property.
|
|
*
|
|
* This is zero if no downloads are currently in progress.
|
|
*/
|
|
progressTotalBytes = 0;
|
|
|
|
/**
|
|
* Number of bytes currently transferred as part of all the downloads that are
|
|
* currently in progress.
|
|
*
|
|
* This is zero if no downloads are currently in progress.
|
|
*/
|
|
progressCurrentBytes = 0;
|
|
|
|
/**
|
|
* This method may be called once to bind this object to a DownloadList.
|
|
*
|
|
* Views on the summarized data can be registered before this object is bound
|
|
* to an actual list. This allows the summary to be used without requiring
|
|
* the initialization of the DownloadList first.
|
|
*
|
|
* @param list
|
|
* Underlying DownloadList whose contents should be summarized.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the view on the underlying list has been registered.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async bindToList(list) {
|
|
if (this._list) {
|
|
throw new Error("bindToList may be called only once.");
|
|
}
|
|
|
|
await list.addView(this);
|
|
// Set the list reference only after addView has returned, so that we don't
|
|
// send a notification to our views for each download that is added.
|
|
this._list = list;
|
|
this._onListChanged();
|
|
}
|
|
|
|
/**
|
|
* Adds a view that will be notified of changes to the summary. The newly
|
|
* added view will receive an initial onSummaryChanged notification.
|
|
*
|
|
* @param view
|
|
* The view object to add. The following methods may be defined:
|
|
* {
|
|
* onSummaryChanged: function () {
|
|
* // Called after any property of the summary has changed.
|
|
* },
|
|
* }
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the view has been registered and the onSummaryChanged
|
|
* notification has been sent.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async addView(view) {
|
|
this._views.add(view);
|
|
|
|
if ("onSummaryChanged" in view) {
|
|
try {
|
|
view.onSummaryChanged();
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes a view that was previously added using addView.
|
|
*
|
|
* @param view
|
|
* The view object to remove.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the view has been removed. At this point, the removed view
|
|
* will not receive any more notifications.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async removeView(view) {
|
|
this._views.delete(view);
|
|
}
|
|
|
|
/**
|
|
* This function is called when any change in the list of downloads occurs,
|
|
* and will recalculate the summary and notify the views in case the
|
|
* aggregated properties are different.
|
|
*/
|
|
_onListChanged() {
|
|
let allHaveStopped = true;
|
|
let allUnknownSize = true;
|
|
let progressTotalBytes = 0;
|
|
let progressCurrentBytes = 0;
|
|
|
|
// Recalculate the aggregated state. See the description of the individual
|
|
// properties for an explanation of the summarization logic.
|
|
for (let download of this._downloads) {
|
|
if (!download.stopped) {
|
|
allHaveStopped = false;
|
|
if (download.hasProgress) {
|
|
allUnknownSize = false;
|
|
progressTotalBytes += download.totalBytes;
|
|
} else {
|
|
progressTotalBytes += download.currentBytes;
|
|
}
|
|
progressCurrentBytes += download.currentBytes;
|
|
}
|
|
}
|
|
|
|
// Exit now if the properties did not change.
|
|
if (
|
|
this.allHaveStopped == allHaveStopped &&
|
|
this.allUnknownSize == allUnknownSize &&
|
|
this.progressTotalBytes == progressTotalBytes &&
|
|
this.progressCurrentBytes == progressCurrentBytes
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.allHaveStopped = allHaveStopped;
|
|
this.allUnknownSize = allUnknownSize;
|
|
this.progressTotalBytes = progressTotalBytes;
|
|
this.progressCurrentBytes = progressCurrentBytes;
|
|
|
|
// Notify all the views that our properties changed.
|
|
for (let view of this._views) {
|
|
try {
|
|
if ("onSummaryChanged" in view) {
|
|
view.onSummaryChanged();
|
|
}
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// DownloadList callback
|
|
onDownloadAdded(download) {
|
|
this._downloads.push(download);
|
|
if (this._list) {
|
|
this._onListChanged();
|
|
}
|
|
}
|
|
|
|
// DownloadList callback
|
|
onDownloadChanged() {
|
|
this._onListChanged();
|
|
}
|
|
|
|
// DownloadList callback
|
|
onDownloadRemoved(download) {
|
|
let index = this._downloads.indexOf(download);
|
|
if (index != -1) {
|
|
this._downloads.splice(index, 1);
|
|
}
|
|
this._onListChanged();
|
|
}
|
|
}
|