diff options
-rw-r--r-- | application/palemoon/components/downloads/content/allDownloadsViewOverlay.js | 232 |
1 files changed, 137 insertions, 95 deletions
diff --git a/application/palemoon/components/downloads/content/allDownloadsViewOverlay.js b/application/palemoon/components/downloads/content/allDownloadsViewOverlay.js index 97a9d242b..580cf6c61 100644 --- a/application/palemoon/components/downloads/content/allDownloadsViewOverlay.js +++ b/application/palemoon/components/downloads/content/allDownloadsViewOverlay.js @@ -38,8 +38,6 @@ const DOWNLOAD_VIEW_SUPPORTED_COMMANDS = "downloadsCmd_open", "downloadsCmd_show", "downloadsCmd_retry", "downloadsCmd_openReferrer", "downloadsCmd_clearDownloads"]; -const NOT_AVAILABLE = Number.MAX_VALUE; - /** * A download element shell is responsible for handling the commands and the * displayed data for a single download view element. The download element @@ -62,22 +60,24 @@ const NOT_AVAILABLE = Number.MAX_VALUE; * The data item of a the session download. Required if aPlacesNode is not set * @param [optional] aPlacesNode * The places node for a past download. Required if aDataItem is not set. - * @param [optional] aAnnotations - * Map containing annotations values, to speed up the initial loading. + * @param [optional] aPlacesMetaData + * Object containing metadata from Places annotations values. + * This is required when a Places node is provided on construction. */ -function DownloadElementShell(aDataItem, aPlacesNode, aAnnotations) { +function DownloadElementShell(aDataItem, aPlacesNode, aPlacesMetaData) { this._element = document.createElement("richlistitem"); this._element._shell = this; this._element.classList.add("download"); this._element.classList.add("download-state"); - if (aAnnotations) - this._annotations = aAnnotations; - if (aDataItem) + if (aDataItem) { this.dataItem = aDataItem; - if (aPlacesNode) + } + if (aPlacesNode) { + this.placesMetaData = aPlacesMetaData; this.placesNode = aPlacesNode; + } } DownloadElementShell.prototype = { @@ -124,12 +124,6 @@ DownloadElementShell.prototype = { if (!aValue && !this._dataItem) throw new Error("Should always have either a dataItem or a placesNode"); - // Preserve the annotations map if this is the first loading and we got - // cached values. - if (this._placesNode || !this._annotations) { - this._annotations = new Map(); - } - this._placesNode = aValue; // We don't need to update the UI if we had a data item, because @@ -171,36 +165,6 @@ DownloadElementShell.prototype = { throw new Error("Unexpected download element state"); }, - // Helper for getting a places annotation set for the download. - _getAnnotation: function DES__getAnnotation(aAnnotation, aDefaultValue) { - let value; - if (this._annotations.has(aAnnotation)) - value = this._annotations.get(aAnnotation); - - // If the value is cached, or we know it doesn't exist, avoid a database - // lookup. - if (value === undefined) { - try { - value = PlacesUtils.annotations.getPageAnnotation( - this._downloadURIObj, aAnnotation); - } - catch(ex) { - value = NOT_AVAILABLE; - } - } - - if (value === NOT_AVAILABLE) { - if (aDefaultValue === undefined) { - throw new Error("Could not get required annotation '" + aAnnotation + - "' for download with url '" + this.downloadURI + "'"); - } - value = aDefaultValue; - } - - this._annotations.set(aAnnotation, value); - return value; - }, - _fetchTargetFileInfo: function DES__fetchTargetFileInfo(aUpdateMetaDataAndStatusUI = false) { if (this._targetFileInfoFetched) throw new Error("_fetchTargetFileInfo should not be called if the information was already fetched"); @@ -257,17 +221,6 @@ DownloadElementShell.prototype = { ); }, - _getAnnotatedMetaData: function DES__getAnnotatedMetaData() - JSON.parse(this._getAnnotation(DOWNLOAD_META_DATA_ANNO)), - - _extractFilePathAndNameFromFileURI: - function DES__extractFilePathAndNameFromFileURI(aFileURI) { - let file = Cc["@mozilla.org/network/protocol;1?name=file"] - .getService(Ci.nsIFileProtocolHandler) - .getFileFromURLSpec(aFileURI); - return [file.path, file.leafName]; - }, - /** * Retrieve the meta data object for the download. The following fields * may be set. @@ -311,13 +264,16 @@ DownloadElementShell.prototype = { } else { try { - this._metaData = this._getAnnotatedMetaData(); + this._metaData = JSON.parse(this.placesMetaData.jsonDetails); } catch(ex) { this._metaData = { }; if (this._targetFileInfoFetched && this._targetFileExists) { - this._metaData.state = this._targetFileSize > 0 ? - nsIDM.DOWNLOAD_FINISHED : nsIDM.DOWNLOAD_FAILED; + // For very old downloads without metadata, we assume that a zero + // byte file is a placeholder, and allow the download to restart. + this._metaData.state = this._targetFileSize > 0 + ? nsIDM.DOWNLOAD_FINISHED + : nsIDM.DOWNLOAD_FAILED; this._metaData.fileSize = this._targetFileSize; } @@ -326,10 +282,13 @@ DownloadElementShell.prototype = { } try { - let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO); - [this._metaData.filePath, this._metaData.fileName] = - this._extractFilePathAndNameFromFileURI(targetFileURI); - this._metaData.displayName = this._metaData.fileName; + let targetFile = Cc["@mozilla.org/network/protocol;1?name=file"] + .getService(Ci.nsIFileProtocolHandler) + .getFileFromURLSpec(this.placesMetaData + .targetFileURISpec); + this._metaData.filePath = targetFile.path; + this._metaData.fileName = targetFile.leafName; + this._metaData.displayName = targetFile.leafName; } catch(ex) { this._metaData.displayName = this.downloadURI; @@ -756,34 +715,95 @@ DownloadsPlacesView.prototype = { return this._active; }, - _getAnnotationsFor: function DPV_getAnnotationsFor(aURI) { - if (!this._cachedAnnotations) { - this._cachedAnnotations = new Map(); - for (let name of [ DESTINATION_FILE_URI_ANNO, - DOWNLOAD_META_DATA_ANNO ]) { - let results = PlacesUtils.annotations.getAnnotationsWithName(name); - for (let result of results) { - let url = result.uri.spec; - if (!this._cachedAnnotations.has(url)) - this._cachedAnnotations.set(url, new Map()); - let m = this._cachedAnnotations.get(url); - m.set(result.annotationName, result.annotationValue); + /** + * Map containing a metadata object for each download source URI found in + * Places annotations, or null if this cache must be rebuilt. + */ + _cachedPlacesMetaData: null, + + /** + * This method exists in order to optimize the first load of the Downloads + * View, 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. + * + * When this metod is first called, after the Places result container has been + * invalidated, it reads the annotations for all the history downloads. + * + * At this point, as metadata for the various elements is requested by the + * view, it is pulled from the cache and returned as an object. + * + * This works efficiently because the metadata is requested only for newly + * created element shells that contain a history download but don't have a + * matching session download, and at most one such element shell is created + * for each source URI. + * + * @param url + * URI string of the Places node for which metadata is requested. + * + * @return Object of the form { targetFileURISpec, jsonDetails }. + * Any of the properties may be missing from the object. + */ + _getCachedPlacesMetaDataFor(url) { + if (!this._cachedPlacesMetaData) { + this._cachedPlacesMetaData = new Map(); + + // Use the destination file annotation to build the initial Map of items. + for (let result of PlacesUtils.annotations.getAnnotationsWithName( + DESTINATION_FILE_URI_ANNO)) { + this._cachedPlacesMetaData.set(result.uri.spec, { + targetFileURISpec: result.annotationValue, + }); + } + + // Add the string with the JSON details from the metadata annotation. + for (let result of PlacesUtils.annotations.getAnnotationsWithName( + DOWNLOAD_META_DATA_ANNO)) { + let metadata = this._cachedPlacesMetaData.get(result.uri.spec); + + // The destination file annotation is expected to be present for all + // the downloads with the metadata annotation. If this is not the case, + // we simply ignore the item, as if it has no JSON details at all. + if (metadata) { + metadata.jsonDetails = result.annotationValue; } } } - let annotations = this._cachedAnnotations.get(aURI); - if (!annotations) { - // There are no annotations for this entry, that means it is quite old. - // Make up a fake annotations entry with default values. - annotations = new Map(); - annotations.set(DESTINATION_FILE_URI_ANNO, NOT_AVAILABLE); - } - // The meta-data annotation has been added recently, so it's likely missing. - if (!annotations.has(DOWNLOAD_META_DATA_ANNO)) { - annotations.set(DOWNLOAD_META_DATA_ANNO, NOT_AVAILABLE); - } - return annotations; + let metadata = this._cachedPlacesMetaData.get(url); + this._cachedPlacesMetaData.delete(url); + + // In the rare case where a history download is added again, but there is no + // corresponding session download, read the metadata from the database. This + // could potentially cause an inefficient database access for very old + // downloads with no metadata to begin with, but these should have been + // already deleted from history on a recent browser profile. + return metadata || this._getPlacesMetaDataFor(url); + }, + + /** + * Read the latest version of the Places metadata for the given URI. + * + * @param url + * URI string of the Places node for which metadata is requested. + * + * @return Object of the form { targetFileURISpec, jsonDetails }. + * Any of the properties may be missing from the object. + */ + _getPlacesMetaDataFor(url) { + let metadata = {}; + + try { + let uri = NetUtil.newURI(url); + metadata = { + targetFileURISpec: PlacesUtils.annotations.getPageAnnotation( + uri, DESTINATION_FILE_URI_ANNO), + }; + metadata.jsonDetails = PlacesUtils.annotations.getPageAnnotation( + uri, DOWNLOAD_META_DATA_ANNO); + } catch (ex) {} + + return metadata; }, /** @@ -861,20 +881,30 @@ DownloadsPlacesView.prototype = { } if (shouldCreateShell) { - // Bug 836271: The annotations for a url should be cached only when the - // places node is available, i.e. when we know we we'd be notified for - // annotation changes. - // Otherwise we may cache NOT_AVILABLE values first for a given session - // download, and later use these NOT_AVILABLE values when a history - // download for the same URL is added. - let cachedAnnotations = aPlacesNode ? this._getAnnotationsFor(downloadURI) : null; - let shell = new DownloadElementShell(aDataItem, aPlacesNode, cachedAnnotations); + // If we are adding a new history download here, it means there is no + // associated session download, thus we must read the Places metadata, + // because it will not be obscured by the session download. + let metaData = aPlacesNode + ? this._getCachedPlacesMetaDataFor(aPlacesNode.uri) + : null; + let shell = new DownloadElementShell(aDataItem, aPlacesNode, metaData); newOrUpdatedShell = shell; shellsForURI.add(shell); if (aDataItem) this._viewItemsForDataItems.set(aDataItem, shell); } else if (aPlacesNode) { + // We are updating information for a history download for which we have + // at least one download element shell already. There are two cases: + // 1) There are one or more download element shells for this source URI, + // each with an associated session download. We update the Places node + // because we may need it later, but we don't need to read the Places + // metadata until the last session download is removed. + // 2) Occasionally, we may receive a duplicate notification for a history + // download with no associated session download. We have exactly one + // download element shell in this case, but the metdata cannot have + // changed, just the reference to the Places node object is different. + // So, we update all the node references and keep the metadata intact. for (let shell of shellsForURI) { if (shell.placesNode != aPlacesNode) shell.placesNode = aPlacesNode; @@ -978,6 +1008,12 @@ DownloadsPlacesView.prototype = { this._downloadElementsShellsForURI.delete(aDataItem.uri); } else { + // We have one download element shell 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 removing the session download. + shell.placesMetaData = this._getPlacesMetaDataFor(shell.placesNode.uri); shell.dataItem = null; // Move it below the session-download items; if (this._lastSessionDownloadElement == shell.element) { @@ -1114,6 +1150,12 @@ DownloadsPlacesView.prototype = { if (!aContainer.containerOpen) throw new Error("Root container for the downloads query cannot be closed"); + // When the container is invalidated, it means we are about to re-read all + // the information about history downloads from the database, discarding the + // download element shells, thus we fully rebuild the Places annotation + // cache with the latest values. + this._cachedPlacesMetaData = null; + let suppressOnSelect = this._richlistbox.suppressOnSelect; this._richlistbox.suppressOnSelect = true; try { |