diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/components/downloads/DownloadHistory.sys.mjs | |
parent | Initial commit. (diff) | |
download | thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/downloads/DownloadHistory.sys.mjs')
-rw-r--r-- | toolkit/components/downloads/DownloadHistory.sys.mjs | 863 |
1 files changed, 863 insertions, 0 deletions
diff --git a/toolkit/components/downloads/DownloadHistory.sys.mjs b/toolkit/components/downloads/DownloadHistory.sys.mjs new file mode 100644 index 0000000000..0077601d84 --- /dev/null +++ b/toolkit/components/downloads/DownloadHistory.sys.mjs @@ -0,0 +1,863 @@ +/* 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 access to downloads from previous sessions on platforms that store + * them in a different location than session downloads. + * + * This module works with objects that are compatible with Download, while using + * the Places interfaces internally. Some of the Places objects may also be + * exposed to allow the consumers to integrate with history view commands. + */ + +import { DownloadList } from "resource://gre/modules/DownloadList.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Downloads: "resource://gre/modules/Downloads.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +// Places query used to retrieve all history downloads for the related list. +const HISTORY_PLACES_QUERY = `place:transition=${Ci.nsINavHistoryService.TRANSITION_DOWNLOAD}&sort=${Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING}`; +const DESTINATIONFILEURI_ANNO = "downloads/destinationFileURI"; +const METADATA_ANNO = "downloads/metaData"; + +const METADATA_STATE_FINISHED = 1; +const METADATA_STATE_FAILED = 2; +const METADATA_STATE_CANCELED = 3; +const METADATA_STATE_PAUSED = 4; +const METADATA_STATE_BLOCKED_PARENTAL = 6; +const METADATA_STATE_DIRTY = 8; + +/** + * Provides methods to retrieve downloads from previous sessions and store + * downloads for future sessions. + */ +export let DownloadHistory = { + /** + * Retrieves the main DownloadHistoryList object which provides a unified view + * on downloads from both previous browsing sessions and this session. + * + * @param type + * Determines which type of downloads from this session should be + * included in the list. This is Downloads.PUBLIC by default, but can + * also be Downloads.PRIVATE or Downloads.ALL. + * @param maxHistoryResults + * Optional number that limits the amount of results the history query + * may return. + * + * @return {Promise} + * @resolves The requested DownloadHistoryList object. + * @rejects JavaScript exception. + */ + async getList({ type = lazy.Downloads.PUBLIC, maxHistoryResults } = {}) { + await DownloadCache.ensureInitialized(); + + let key = `${type}|${maxHistoryResults ? maxHistoryResults : -1}`; + if (!this._listPromises[key]) { + this._listPromises[key] = lazy.Downloads.getList(type).then(list => { + // When the amount of history downloads is capped, we request the list in + // descending order, to make sure that the list can apply the limit. + let query = + HISTORY_PLACES_QUERY + + (maxHistoryResults ? `&maxResults=${maxHistoryResults}` : ""); + + return new DownloadHistoryList(list, query); + }); + } + + return this._listPromises[key]; + }, + + /** + * This object is populated with one key for each type of download list that + * can be returned by the getList method. The values are promises that resolve + * to DownloadHistoryList objects. + */ + _listPromises: {}, + + async addDownloadToHistory(download) { + if ( + download.source.isPrivate || + !lazy.PlacesUtils.history.canAddURI( + lazy.PlacesUtils.toURI(download.source.url) + ) + ) { + return; + } + + await DownloadCache.addDownload(download); + + await this._updateHistoryListData(download.source.url); + }, + + /** + * Stores new detailed metadata for the given download in history. This is + * normally called after a download finishes, fails, or is canceled. + * + * Failed or canceled downloads with partial data are not stored as paused, + * because the information from the session download is required for resuming. + * + * @param download + * Download object whose metadata should be updated. If the object + * represents a private download, the call has no effect. + */ + async updateMetaData(download) { + if ( + download.source.isPrivate || + !download.stopped || + !lazy.PlacesUtils.history.canAddURI( + lazy.PlacesUtils.toURI(download.source.url) + ) + ) { + return; + } + + let state = METADATA_STATE_CANCELED; + if (download.succeeded) { + state = METADATA_STATE_FINISHED; + } else if (download.error) { + if (download.error.becauseBlockedByParentalControls) { + state = METADATA_STATE_BLOCKED_PARENTAL; + } else if (download.error.becauseBlockedByReputationCheck) { + state = METADATA_STATE_DIRTY; + } else { + state = METADATA_STATE_FAILED; + } + } + + let metaData = { + state, + deleted: download.deleted, + endTime: download.endTime, + }; + if (download.succeeded) { + metaData.fileSize = download.target.size; + } + + // The verdict may still be present even if the download succeeded. + if (download.error && download.error.reputationCheckVerdict) { + metaData.reputationCheckVerdict = download.error.reputationCheckVerdict; + } + + // This should be executed before any async parts, to ensure the cache is + // updated before any notifications are activated. + await DownloadCache.setMetadata(download.source.url, metaData); + + await this._updateHistoryListData(download.source.url); + }, + + async _updateHistoryListData(sourceUrl) { + for (let key of Object.getOwnPropertyNames(this._listPromises)) { + let downloadHistoryList = await this._listPromises[key]; + downloadHistoryList.updateForMetaDataChange( + sourceUrl, + DownloadCache.get(sourceUrl) + ); + } + }, +}; + +/** + * This cache exists: + * - in order to optimize the load of DownloadsHistoryList, when Places + * annotations for history downloads must be read. In fact, annotations are + * stored in a single table, and reading all of them at once is much more + * efficient than an individual query. + * - to avoid needing to do asynchronous reading of the database during download + * list updates, which are designed to be synchronous (to improve UI + * responsiveness). + * + * The cache is initialized the first time DownloadHistory.getList is called, or + * when data is added. + */ +let DownloadCache = { + _data: new Map(), + _initializePromise: null, + + /** + * Initializes the cache, loading the data from the places database. + * + * @return {Promise} Returns a promise that is resolved once the + * initialization is complete. + */ + ensureInitialized() { + if (this._initializePromise) { + return this._initializePromise; + } + this._initializePromise = (async () => { + const placesObserver = new PlacesWeakCallbackWrapper( + this.handlePlacesEvents.bind(this) + ); + PlacesObservers.addListener( + ["history-cleared", "page-removed"], + placesObserver + ); + + let pageAnnos = await lazy.PlacesUtils.history.fetchAnnotatedPages([ + METADATA_ANNO, + DESTINATIONFILEURI_ANNO, + ]); + + let metaDataPages = pageAnnos.get(METADATA_ANNO); + if (metaDataPages) { + for (let { uri, content } of metaDataPages) { + try { + this._data.set(uri.href, JSON.parse(content)); + } catch (ex) { + // Do nothing - JSON.parse could throw. + } + } + } + + let destinationFilePages = pageAnnos.get(DESTINATIONFILEURI_ANNO); + if (destinationFilePages) { + for (let { uri, content } of destinationFilePages) { + let newData = this.get(uri.href); + newData.targetFileSpec = content; + this._data.set(uri.href, newData); + } + } + })(); + + return this._initializePromise; + }, + + /** + * This returns an object containing the meta data for the supplied URL. + * + * @param {String} url The url to get the meta data for. + * @return {Object|null} Returns an empty object if there is no meta data found, or + * an object containing the meta data. The meta data + * will look like: + * + * { targetFileSpec, state, deleted, endTime, fileSize, ... } + * + * The targetFileSpec property is the value of "downloads/destinationFileURI", + * while the other properties are taken from "downloads/metaData". Any of the + * properties may be missing from the object. + */ + get(url) { + return this._data.get(url) || {}; + }, + + /** + * Adds a download to the cache and the places database. + * + * @param {Download} download The download to add to the database and cache. + */ + async addDownload(download) { + await this.ensureInitialized(); + + let targetFile = new lazy.FileUtils.File(download.target.path); + let targetUri = Services.io.newFileURI(targetFile); + + // This should be executed before any async parts, to ensure the cache is + // updated before any notifications are activated. + // Note: this intentionally overwrites any metadata as this is + // the start of a new download. + this._data.set(download.source.url, { targetFileSpec: targetUri.spec }); + + let originalPageInfo = await lazy.PlacesUtils.history.fetch( + download.source.url + ); + + let pageInfo = await lazy.PlacesUtils.history.insert({ + url: download.source.url, + // In case we are downloading a file that does not correspond to a web + // page for which the title is present, we populate the otherwise empty + // history title with the name of the destination file, to allow it to be + // visible and searchable in history results. + title: + (originalPageInfo && originalPageInfo.title) || targetFile.leafName, + visits: [ + { + // The start time is always available when we reach this point. + date: download.startTime, + transition: lazy.PlacesUtils.history.TRANSITIONS.DOWNLOAD, + referrer: download.source.referrerInfo + ? download.source.referrerInfo.originalReferrer + : null, + }, + ], + }); + + await lazy.PlacesUtils.history.update({ + annotations: new Map([["downloads/destinationFileURI", targetUri.spec]]), + // XXX Bug 1479445: We shouldn't have to supply both guid and url here, + // but currently we do. + guid: pageInfo.guid, + url: pageInfo.url, + }); + }, + + /** + * Sets the metadata for a given url. If the cache already contains meta data + * for the given url, it will be overwritten (note: the targetFileSpec will be + * maintained). + * + * @param {String} url The url to set the meta data for. + * @param {Object} metadata The new metaData to save in the cache. + */ + async setMetadata(url, metadata) { + await this.ensureInitialized(); + + // This should be executed before any async parts, to ensure the cache is + // updated before any notifications are activated. + let existingData = this.get(url); + let newData = { ...metadata }; + if ("targetFileSpec" in existingData) { + newData.targetFileSpec = existingData.targetFileSpec; + } + this._data.set(url, newData); + + try { + await lazy.PlacesUtils.history.update({ + annotations: new Map([[METADATA_ANNO, JSON.stringify(metadata)]]), + url, + }); + } catch (ex) { + console.error(ex); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]), + + handlePlacesEvents(events) { + for (const event of events) { + switch (event.type) { + case "history-cleared": { + this._data.clear(); + break; + } + case "page-removed": { + if (event.isRemovedFromStore) { + this._data.delete(event.url); + } + break; + } + } + } + }, +}; + +/** + * Represents a download from the browser history. This object implements part + * of the interface of the Download object. + * + * While Download objects are shared between the public DownloadList and all the + * DownloadHistoryList instances, multiple HistoryDownload objects referring to + * the same item can be created for different DownloadHistoryList instances. + * + * @param placesNode + * The Places node from which the history download should be initialized. + */ +class HistoryDownload { + constructor(placesNode) { + this.placesNode = placesNode; + + // History downloads should get the referrer from Places (bug 829201). + this.source = { + url: placesNode.uri, + isPrivate: false, + }; + this.target = { + path: undefined, + exists: false, + size: undefined, + }; + + // In case this download cannot obtain its end time from the Places metadata, + // use the time from the Places node, that is the start time of the download. + this.endTime = placesNode.time / 1000; + } + + /** + * DownloadSlot containing this history download. + * + * @type {DownloadSlot} + */ + slot = null; + + /** + * History downloads are never in progress. + * + * @type {Boolean} + */ + stopped = true; + + /** + * No percentage indication is shown for history downloads. + * + * @type {Boolean} + */ + hasProgress = false; + + /** + * History downloads cannot be restarted using their partial data, even if + * they are indicated as paused in their Places metadata. The only way is to + * use the information from a persisted session download, that will be shown + * instead of the history download. In case this session download is not + * available, we show the history download as canceled, not paused. + * + * @type {Boolean} + */ + hasPartialData = false; + + /** + * Pushes information from Places metadata into this object. + */ + updateFromMetaData(metaData) { + try { + this.target.path = Cc["@mozilla.org/network/protocol;1?name=file"] + .getService(Ci.nsIFileProtocolHandler) + .getFileFromURLSpec(metaData.targetFileSpec).path; + } catch (ex) { + this.target.path = undefined; + } + + if ("state" in metaData) { + this.succeeded = metaData.state == METADATA_STATE_FINISHED; + this.canceled = + metaData.state == METADATA_STATE_CANCELED || + metaData.state == METADATA_STATE_PAUSED; + this.endTime = metaData.endTime; + this.deleted = metaData.deleted; + + // Recreate partial error information from the state saved in history. + if (metaData.state == METADATA_STATE_FAILED) { + this.error = { message: "History download failed." }; + } else if (metaData.state == METADATA_STATE_BLOCKED_PARENTAL) { + this.error = { becauseBlockedByParentalControls: true }; + } else if (metaData.state == METADATA_STATE_DIRTY) { + this.error = { + becauseBlockedByReputationCheck: true, + reputationCheckVerdict: metaData.reputationCheckVerdict || "", + }; + } else { + this.error = null; + } + + // Normal history downloads are assumed to exist until the user interface + // is refreshed, at which point these values may be updated. + this.target.exists = true; + this.target.size = metaData.fileSize; + } else { + // Metadata might be missing from a download that has started but hasn't + // stopped already. Normally, this state is overridden with the one from + // the corresponding in-progress session download. But if the browser is + // terminated abruptly and additionally the file with information about + // in-progress downloads is lost, we may end up using this state. We use + // the failed state to allow the download to be restarted. + // + // On the other hand, if the download is missing the target file + // annotation as well, it is just a very old one, and we can assume it + // succeeded. + this.succeeded = !this.target.path; + this.error = this.target.path ? { message: "Unstarted download." } : null; + this.canceled = false; + this.deleted = false; + + // These properties may be updated if the user interface is refreshed. + this.target.exists = false; + this.target.size = undefined; + } + } + + /** + * This method may be called when deleting a history download. + */ + async finalize() {} + + /** + * This method mimicks the "refresh" method of session downloads. + */ + async refresh() { + try { + this.target.size = (await IOUtils.stat(this.target.path)).size; + this.target.exists = true; + } catch (ex) { + // We keep the known file size from the metadata, if any. + this.target.exists = false; + } + + this.slot.list._notifyAllViews("onDownloadChanged", this); + } + + /** + * This method mimicks the "manuallyRemoveData" method of session downloads. + */ + async manuallyRemoveData() { + let { path } = this.target; + if (this.target.path && this.succeeded) { + // Temp files are made "read-only" by DownloadIntegration.downloadDone, so + // reset the permission bits to read/write. This won't be necessary after + // bug 1733587 since Downloads won't ever be temporary. + await IOUtils.setPermissions(path, 0o660); + await IOUtils.remove(path, { ignoreAbsent: true }); + } + this.deleted = true; + await this.refresh(); + } +} + +/** + * Represents one item in the list of public session and history downloads. + * + * The object may contain a session download, a history download, or both. When + * both a history and a session download are present, the session download gets + * priority and its information is accessed. + * + * @param list + * The DownloadHistoryList that owns this DownloadSlot object. + */ +class DownloadSlot { + constructor(list) { + this.list = list; + } + + /** + * Download object representing the session download contained in this slot. + */ + sessionDownload = null; + _historyDownload = null; + + /** + * HistoryDownload object contained in this slot. + */ + get historyDownload() { + return this._historyDownload; + } + + set historyDownload(historyDownload) { + this._historyDownload = historyDownload; + if (historyDownload) { + historyDownload.slot = this; + } + } + + /** + * Returns the Download or HistoryDownload object for displaying information + * and executing commands in the user interface. + */ + get download() { + return this.sessionDownload || this.historyDownload; + } +} + +/** + * Represents an ordered collection of DownloadSlot objects containing a merged + * view on session downloads and history downloads. Views on this list will + * receive notifications for changes to both types of downloads. + * + * Downloads in this list are sorted from oldest to newest, with all session + * downloads after all the history downloads. When a new history download is + * added and the list also contains session downloads, the insertBefore option + * of the onDownloadAdded notification refers to the first session download. + * + * The list of downloads cannot be modified using the DownloadList methods. + * + * @param publicList + * Underlying DownloadList containing public downloads. + * @param place + * Places query used to retrieve history downloads. + */ +class DownloadHistoryList extends DownloadList { + constructor(publicList, place) { + super(); + + // While "this._slots" contains all the data in order, the other properties + // provide fast access for the most common operations. + this._slots = []; + this._slotsForUrl = new Map(); + this._slotForDownload = new WeakMap(); + + // Start the asynchronous queries to retrieve history and session downloads. + publicList.addView(this).catch(console.error); + let query = {}, + options = {}; + lazy.PlacesUtils.history.queryStringToQuery(place, query, options); + + // NB: The addObserver call sets our nsINavHistoryResultObserver.result. + let result = lazy.PlacesUtils.history.executeQuery( + query.value, + options.value + ); + result.addObserver(this); + + // Our history result observer is long lived for fast shared views, so free + // the reference on shutdown to prevent leaks. + Services.obs.addObserver(() => { + this.result = null; + }, "quit-application-granted"); + } + + /** + * This is set when executing the Places query. + */ + _result = null; + + /** + * Index of the first slot that contains a session download. This is equal to + * the length of the list when there are no session downloads. + * + * @type {Number} + */ + _firstSessionSlotIndex = 0; + + get result() { + return this._result; + } + + set result(result) { + if (this._result == result) { + return; + } + + if (this._result) { + this._result.removeObserver(this); + this._result.root.containerOpen = false; + } + + this._result = result; + + if (this._result) { + this._result.root.containerOpen = true; + } + } + + /** + * Updates the download history item when the meta data or destination file + * changes. + * + * @param {String} sourceUrl The sourceUrl which was updated. + * @param {Object} metaData The new meta data for the sourceUrl. + */ + updateForMetaDataChange(sourceUrl, metaData) { + let slotsForUrl = this._slotsForUrl.get(sourceUrl); + if (!slotsForUrl) { + return; + } + + for (let slot of slotsForUrl) { + if (slot.sessionDownload) { + // The visible data doesn't change, so we don't have to notify views. + return; + } + slot.historyDownload.updateFromMetaData(metaData); + this._notifyAllViews("onDownloadChanged", slot.download); + } + } + + _insertSlot({ slot, index, slotsForUrl }) { + // Add the slot to the ordered array. + this._slots.splice(index, 0, slot); + this._downloads.splice(index, 0, slot.download); + if (!slot.sessionDownload) { + this._firstSessionSlotIndex++; + } + + // Add the slot to the fast access maps. + slotsForUrl.add(slot); + this._slotsForUrl.set(slot.download.source.url, slotsForUrl); + + // Add the associated view items. + this._notifyAllViews("onDownloadAdded", slot.download, { + insertBefore: this._downloads[index + 1], + }); + } + + _removeSlot({ slot, slotsForUrl }) { + // Remove the slot from the ordered array. + let index = this._slots.indexOf(slot); + this._slots.splice(index, 1); + this._downloads.splice(index, 1); + if (this._firstSessionSlotIndex > index) { + this._firstSessionSlotIndex--; + } + + // Remove the slot from the fast access maps. + slotsForUrl.delete(slot); + if (slotsForUrl.size == 0) { + this._slotsForUrl.delete(slot.download.source.url); + } + + // Remove the associated view items. + this._notifyAllViews("onDownloadRemoved", slot.download); + } + + /** + * Ensures that the information about a history download is stored in at least + * one slot, adding a new one at the end of the list if necessary. + * + * A reference to the same Places node will be stored in the HistoryDownload + * object for all the DownloadSlot objects associated with the source URL. + * + * @param placesNode + * The Places node that represents the history download. + */ + _insertPlacesNode(placesNode) { + let slotsForUrl = this._slotsForUrl.get(placesNode.uri) || new Set(); + + // If there are existing slots associated with this URL, we only have to + // ensure that the Places node reference is kept updated in case the more + // recent Places notification contained a different node object. + if (slotsForUrl.size > 0) { + for (let slot of slotsForUrl) { + if (!slot.historyDownload) { + slot.historyDownload = new HistoryDownload(placesNode); + } else { + slot.historyDownload.placesNode = placesNode; + } + } + return; + } + + // If there are no existing slots for this URL, we have to create a new one. + // Since the history download is visible in the slot, we also have to update + // the object using the Places metadata. + let historyDownload = new HistoryDownload(placesNode); + historyDownload.updateFromMetaData(DownloadCache.get(placesNode.uri)); + let slot = new DownloadSlot(this); + slot.historyDownload = historyDownload; + this._insertSlot({ slot, slotsForUrl, index: this._firstSessionSlotIndex }); + } + + // nsINavHistoryResultObserver + containerStateChanged(node, oldState, newState) { + this.invalidateContainer(node); + } + + // nsINavHistoryResultObserver + invalidateContainer(container) { + this._notifyAllViews("onDownloadBatchStarting"); + + // Remove all the current slots containing only history downloads. + for (let index = this._slots.length - 1; index >= 0; index--) { + let slot = this._slots[index]; + if (slot.sessionDownload) { + // The visible data doesn't change, so we don't have to notify views. + slot.historyDownload = null; + } else { + let slotsForUrl = this._slotsForUrl.get(slot.download.source.url); + this._removeSlot({ slot, slotsForUrl }); + } + } + + // Add new slots or reuse existing ones for history downloads. + for (let index = container.childCount - 1; index >= 0; --index) { + try { + this._insertPlacesNode(container.getChild(index)); + } catch (ex) { + console.error(ex); + } + } + + this._notifyAllViews("onDownloadBatchEnded"); + } + + // nsINavHistoryResultObserver + nodeInserted(parent, placesNode) { + this._insertPlacesNode(placesNode); + } + + // nsINavHistoryResultObserver + nodeRemoved(parent, placesNode, aOldIndex) { + let slotsForUrl = this._slotsForUrl.get(placesNode.uri); + for (let slot of slotsForUrl) { + if (slot.sessionDownload) { + // The visible data doesn't change, so we don't have to notify views. + slot.historyDownload = null; + } else { + this._removeSlot({ slot, slotsForUrl }); + } + } + } + + // nsINavHistoryResultObserver + nodeIconChanged() {} + nodeTitleChanged() {} + nodeKeywordChanged() {} + nodeDateAddedChanged() {} + nodeLastModifiedChanged() {} + nodeHistoryDetailsChanged() {} + nodeTagsChanged() {} + sortingChanged() {} + nodeMoved() {} + nodeURIChanged() {} + batching() {} + + // DownloadList callback + onDownloadAdded(download) { + let url = download.source.url; + let slotsForUrl = this._slotsForUrl.get(url) || new Set(); + + // For every source URL, there can be at most one slot containing a history + // download without an associated session download. If we find one, then we + // can reuse it for the current session download, although we have to move + // it together with the other session downloads. + let slot = [...slotsForUrl][0]; + if (slot && !slot.sessionDownload) { + // Remove the slot because we have to change its position. + this._removeSlot({ slot, slotsForUrl }); + } else { + slot = new DownloadSlot(this); + } + slot.sessionDownload = download; + this._insertSlot({ slot, slotsForUrl, index: this._slots.length }); + this._slotForDownload.set(download, slot); + } + + // DownloadList callback + onDownloadChanged(download) { + let slot = this._slotForDownload.get(download); + this._notifyAllViews("onDownloadChanged", slot.download); + } + + // DownloadList callback + onDownloadRemoved(download) { + let url = download.source.url; + let slotsForUrl = this._slotsForUrl.get(url); + let slot = this._slotForDownload.get(download); + this._removeSlot({ slot, slotsForUrl }); + + this._slotForDownload.delete(download); + + // If there was only one slot for this source URL and it also contained a + // history download, we should resurrect it in the correct area of the list. + if (slotsForUrl.size == 0 && slot.historyDownload) { + // We have one download slot containing both a session download and a + // history download, and we are now removing the session download. + // Previously, we did not use the Places metadata because it was obscured + // by the session download. Since this is no longer the case, we have to + // read the latest metadata before resurrecting the history download. + slot.historyDownload.updateFromMetaData(DownloadCache.get(url)); + slot.sessionDownload = null; + // Place the resurrected history slot after all the session slots. + this._insertSlot({ + slot, + slotsForUrl, + index: this._firstSessionSlotIndex, + }); + } + } + + // DownloadList + add() { + throw new Error("Not implemented."); + } + + // DownloadList + remove() { + throw new Error("Not implemented."); + } + + // DownloadList + removeFinished() { + throw new Error("Not implemented."); + } +} |