diff options
Diffstat (limited to 'toolkit/components/thumbnails/PageThumbs.sys.mjs')
-rw-r--r-- | toolkit/components/thumbnails/PageThumbs.sys.mjs | 887 |
1 files changed, 887 insertions, 0 deletions
diff --git a/toolkit/components/thumbnails/PageThumbs.sys.mjs b/toolkit/components/thumbnails/PageThumbs.sys.mjs new file mode 100644 index 0000000000..4d103c8bd8 --- /dev/null +++ b/toolkit/components/thumbnails/PageThumbs.sys.mjs @@ -0,0 +1,887 @@ +/* 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/. */ + +const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version"; +const LATEST_STORAGE_VERSION = 3; + +const EXPIRATION_MIN_CHUNK_SIZE = 50; +const EXPIRATION_INTERVAL_SECS = 3600; + +// If a request for a thumbnail comes in and we find one that is "stale" +// (or don't find one at all) we automatically queue a request to generate a +// new one. +const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs. + +/** + * Name of the directory in the profile that contains the thumbnails. + */ +const THUMBNAIL_DIRECTORY = "thumbnails"; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { BasePromiseWorker } from "resource://gre/modules/PromiseWorker.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PageThumbUtils: "resource://gre/modules/PageThumbUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gUpdateTimerManager", + "@mozilla.org/updates/timer-manager;1", + "nsIUpdateTimerManager" +); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "PageThumbsStorageService", + "@mozilla.org/thumbnails/pagethumbs-service;1", + "nsIPageThumbsStorageService" +); + +/** + * Utilities for dealing with promises. + */ +const TaskUtils = { + /** + * Read the bytes from a blob, asynchronously. + * + * @return {Promise} + * @resolve {ArrayBuffer} In case of success, the bytes contained in the blob. + * @reject {DOMException} In case of error, the underlying DOMException. + */ + readBlob: function readBlob(blob) { + return new Promise((resolve, reject) => { + let reader = new FileReader(); + reader.onloadend = function onloadend() { + if (reader.readyState != FileReader.DONE) { + reject(reader.error); + } else { + resolve(reader.result); + } + }; + reader.readAsArrayBuffer(blob); + }); + }, +}; + +/** + * Singleton providing functionality for capturing web page thumbnails and for + * accessing them if already cached. + */ +export var PageThumbs = { + _initialized: false, + + /** + * The calculated width and height of the thumbnails. + */ + _thumbnailWidth: 0, + _thumbnailHeight: 0, + + /** + * The scheme to use for thumbnail urls. + */ + get scheme() { + return "moz-page-thumb"; + }, + + /** + * The static host to use for thumbnail urls. + */ + get staticHost() { + return "thumbnails"; + }, + + /** + * The thumbnails' image type. + */ + get contentType() { + return "image/png"; + }, + + init: function PageThumbs_init() { + if (!this._initialized) { + this._initialized = true; + + this._placesObserver = new PlacesWeakCallbackWrapper( + this.handlePlacesEvents.bind(this) + ); + PlacesObservers.addListener( + ["history-cleared", "page-removed"], + this._placesObserver + ); + + // Migrate the underlying storage, if needed. + PageThumbsStorageMigrator.migrate(); + PageThumbsExpiration.init(); + } + }, + + handlePlacesEvents(events) { + for (const event of events) { + switch (event.type) { + case "history-cleared": { + PageThumbsStorage.wipe(); + break; + } + case "page-removed": { + if (event.isRemovedFromStore) { + PageThumbsStorage.remove(event.url); + } + break; + } + } + } + }, + + uninit: function PageThumbs_uninit() { + if (this._initialized) { + this._initialized = false; + } + }, + + /** + * Gets the thumbnail image's url for a given web page's url. + * @param aUrl The web page's url that is depicted in the thumbnail. + * @return The thumbnail image's url. + */ + getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) { + return ( + this.scheme + + "://" + + this.staticHost + + "/?url=" + + encodeURIComponent(aUrl) + + "&revision=" + + PageThumbsStorage.getRevision(aUrl) + ); + }, + + /** + * Gets the path of the thumbnail file for a given web page's + * url. This file may or may not exist depending on whether the + * thumbnail has been captured or not. + * + * @param aUrl The web page's url. + * @return The path of the thumbnail file. + */ + getThumbnailPath: function PageThumbs_getThumbnailPath(aUrl) { + return lazy.PageThumbsStorageService.getFilePathForURL(aUrl); + }, + + /** + * Asynchronously returns a thumbnail as a blob for the given + * window. + * + * @param aBrowser The <browser> to capture a thumbnail from. + * @param aArgs See captureToCanvas for accepted arguments. + * @return {Promise} + * @resolve {Blob} The thumbnail, as a Blob. + */ + captureToBlob: function PageThumbs_captureToBlob(aBrowser, aArgs) { + if (!this._prefEnabled()) { + return null; + } + + return new Promise(resolve => { + let canvas = this.createCanvas(aBrowser.ownerGlobal); + this.captureToCanvas(aBrowser, canvas, aArgs) + .then(() => { + canvas.toBlob(blob => { + resolve(blob, this.contentType); + }); + }) + .catch(e => console.error(e)); + }); + }, + + /** + * Captures a thumbnail from a given window and draws it to the given canvas. + * Note, when dealing with remote content, this api draws into the passed + * canvas asynchronously. Pass aCallback to receive an async callback after + * canvas painting has completed. + * @param aBrowser The browser to capture a thumbnail from. + * @param aCanvas The canvas to draw to. The thumbnail will be scaled to match + * the dimensions of this canvas. If callers pass a 0x0 canvas, the canvas + * will be resized to default thumbnail dimensions just prior to painting. + * @param aArgs (optional) Additional named parameters: + * fullScale - request that a non-downscaled image be returned. + * isImage - indicate that this should be treated as an image url. + * backgroundColor - background color to draw behind images. + * targetWidth - desired width for images. + * isBackgroundThumb - true if request is from the background thumb service. + * fullViewport - request that a screenshot for the viewport be + * captured. This makes it possible to get a screenshot that reflects + * the current scroll position of aBrowser. + * @param aSkipTelemetry skip recording telemetry + */ + async captureToCanvas(aBrowser, aCanvas, aArgs, aSkipTelemetry = false) { + let telemetryCaptureTime = new Date(); + let args = { + fullScale: aArgs ? aArgs.fullScale : false, + isImage: aArgs ? aArgs.isImage : false, + backgroundColor: + aArgs?.backgroundColor ?? lazy.PageThumbUtils.THUMBNAIL_BG_COLOR, + targetWidth: + aArgs?.targetWidth ?? lazy.PageThumbUtils.THUMBNAIL_DEFAULT_SIZE, + isBackgroundThumb: aArgs ? aArgs.isBackgroundThumb : false, + fullViewport: aArgs?.fullViewport ?? false, + }; + + return this._captureToCanvas(aBrowser, aCanvas, args).then(() => { + if (!aSkipTelemetry) { + Services.telemetry + .getHistogramById("FX_THUMBNAILS_CAPTURE_TIME_MS") + .add(new Date() - telemetryCaptureTime); + } + return aCanvas; + }); + }, + + /** + * Asynchronously check the state of aBrowser to see if it passes a set of + * predefined security checks. Consumers should refrain from storing + * thumbnails if these checks fail. Note the final result of this call is + * transitory as it is based on current navigation state and the type of + * content being displayed. + * + * @param aBrowser The target browser + */ + async shouldStoreThumbnail(aBrowser) { + // Don't capture in private browsing mode. + if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) { + return false; + } + if (aBrowser.isRemoteBrowser) { + if (aBrowser.browsingContext.currentWindowGlobal) { + let thumbnailsActor = + aBrowser.browsingContext.currentWindowGlobal.getActor("Thumbnails"); + return thumbnailsActor + .sendQuery("Browser:Thumbnail:CheckState") + .catch(err => { + return false; + }); + } + return false; + } + return lazy.PageThumbUtils.shouldStoreContentThumbnail( + aBrowser.contentDocument, + aBrowser.docShell + ); + }, + + // The background thumbnail service captures to canvas but doesn't want to + // participate in this service's telemetry, which is why this method exists. + async _captureToCanvas(aBrowser, aCanvas, aArgs) { + if (aBrowser.isRemoteBrowser) { + let thumbnail = await this._captureRemoteThumbnail( + aBrowser, + aCanvas.width, + aCanvas.height, + aArgs + ); + + // 'thumbnail' can be null if the browser has navigated away after starting + // the thumbnail request, so we check it here. + if (thumbnail) { + let ctx = thumbnail.getContext("2d"); + let imgData = ctx.getImageData(0, 0, thumbnail.width, thumbnail.height); + aCanvas.width = thumbnail.width; + aCanvas.height = thumbnail.height; + aCanvas.getContext("2d").putImageData(imgData, 0, 0); + } + + return aCanvas; + } + // The content is a local page, grab a thumbnail sync. + await lazy.PageThumbUtils.createSnapshotThumbnail(aBrowser, aCanvas, aArgs); + return aCanvas; + }, + + /** + * Asynchrnously render an appropriately scaled thumbnail to canvas. + * + * @param aBrowser The browser to capture a thumbnail from. + * @param aWidth The desired canvas width. + * @param aHeight The desired canvas height. + * @param aArgs (optional) Additional named parameters: + * fullScale - request that a non-downscaled image be returned. + * isImage - indicate that this should be treated as an image url. + * backgroundColor - background color to draw behind images. + * targetWidth - desired width for images. + * isBackgroundThumb - true if request is from the background thumb service. + * fullViewport - request that a screenshot for the viewport be + * captured. This makes it possible to get a screenshot that reflects + * the current scroll position of aBrowser. + * @return a promise + */ + async _captureRemoteThumbnail(aBrowser, aWidth, aHeight, aArgs) { + if (!aBrowser.browsingContext || !aBrowser.isConnected) { + return null; + } + + let thumbnailsActor = aBrowser.browsingContext.currentWindowGlobal.getActor( + aArgs.isBackgroundThumb ? "BackgroundThumbnails" : "Thumbnails" + ); + let contentInfo = await thumbnailsActor.sendQuery( + "Browser:Thumbnail:ContentInfo", + { + isImage: aArgs.isImage, + targetWidth: aArgs.targetWidth, + backgroundColor: aArgs.backgroundColor, + } + ); + + let contentWidth = contentInfo.width; + let contentHeight = contentInfo.height; + if (contentWidth == 0 || contentHeight == 0) { + throw new Error("IMAGE_ZERO_DIMENSION"); + } + + if (!aBrowser.isConnected) { + return null; + } + let doc = aBrowser.ownerDocument; + let thumbnail = doc.createElementNS( + lazy.PageThumbUtils.HTML_NAMESPACE, + "canvas" + ); + + let image; + if (contentInfo.imageData) { + thumbnail.width = contentWidth; + thumbnail.height = contentHeight; + + image = new aBrowser.ownerGlobal.Image(); + await new Promise(resolve => { + image.onload = resolve; + image.src = contentInfo.imageData; + }); + } else { + let fullScale = aArgs ? aArgs.fullScale : false; + let scale = fullScale + ? 1 + : Math.min(Math.max(aWidth / contentWidth, aHeight / contentHeight), 1); + + image = await aBrowser.drawSnapshot( + 0, + 0, + contentWidth, + contentHeight, + scale, + aArgs.backgroundColor, + aArgs.fullViewport + ); + if (!image) { + return null; + } + + thumbnail.width = fullScale ? contentWidth : aWidth; + thumbnail.height = fullScale ? contentHeight : aHeight; + } + + thumbnail.getContext("2d").drawImage(image, 0, 0); + + return thumbnail; + }, + + /** + * Captures a thumbnail for the given browser and stores it to the cache. + * @param aBrowser The browser to capture a thumbnail for. + */ + captureAndStore: async function PageThumbs_captureAndStore(aBrowser) { + if (!this._prefEnabled()) { + return; + } + + let url = aBrowser.currentURI.spec; + let originalURL; + let channelError = false; + + if (!aBrowser.isRemoteBrowser) { + let channel = aBrowser.docShell.currentDocumentChannel; + originalURL = channel.originalURI.spec; + // see if this was an error response. + channelError = lazy.PageThumbUtils.isChannelErrorResponse(channel); + } else { + let thumbnailsActor = + aBrowser.browsingContext.currentWindowGlobal.getActor("Thumbnails"); + let resp = await thumbnailsActor.sendQuery( + "Browser:Thumbnail:GetOriginalURL" + ); + + originalURL = resp.originalURL || url; + channelError = resp.channelError; + } + + try { + let blob = await this.captureToBlob(aBrowser); + let buffer = await TaskUtils.readBlob(blob); + await this._store(originalURL, url, buffer, channelError); + } catch (ex) { + console.error("Exception thrown during thumbnail capture:", ex); + } + }, + + /** + * Checks if an existing thumbnail for the specified URL is either missing + * or stale, and if so, captures and stores it. Once the thumbnail is stored, + * an observer service notification will be sent, so consumers should observe + * such notifications if they want to be notified of an updated thumbnail. + * + * @param aBrowser The content window of this browser will be captured. + */ + captureAndStoreIfStale: async function PageThumbs_captureAndStoreIfStale( + aBrowser + ) { + if (!aBrowser.currentURI) { + return false; + } + let url = aBrowser.currentURI.spec; + let recent; + try { + recent = await PageThumbsStorage.isFileRecentForURL(url); + } catch { + return false; + } + if ( + !recent && + // Careful, the call to PageThumbsStorage is async, so the browser may + // have navigated away from the URL or even closed. + aBrowser.currentURI && + aBrowser.currentURI.spec == url + ) { + await this.captureAndStore(aBrowser); + } + return true; + }, + + /** + * Stores data to disk for the given URLs. + * + * NB: The background thumbnail service calls this, too. + * + * @param aOriginalURL The URL with which the capture was initiated. + * @param aFinalURL The URL to which aOriginalURL ultimately resolved. + * @param aData An ArrayBuffer containing the image data. + * @param aNoOverwrite If true and files for the URLs already exist, the files + * will not be overwritten. + */ + _store: async function PageThumbs__store( + aOriginalURL, + aFinalURL, + aData, + aNoOverwrite + ) { + let telemetryStoreTime = new Date(); + await PageThumbsStorage.writeData(aFinalURL, aData, aNoOverwrite); + Services.telemetry + .getHistogramById("FX_THUMBNAILS_STORE_TIME_MS") + .add(new Date() - telemetryStoreTime); + + Services.obs.notifyObservers(null, "page-thumbnail:create", aFinalURL); + // We've been redirected. Create a copy of the current thumbnail for + // the redirect source. We need to do this because: + // + // 1) Users can drag any kind of links onto the newtab page. If those + // links redirect to a different URL then we want to be able to + // provide thumbnails for both of them. + // + // 2) The newtab page should actually display redirect targets, only. + // Because of bug 559175 this information can get lost when using + // Sync and therefore also redirect sources appear on the newtab + // page. We also want thumbnails for those. + if (aFinalURL != aOriginalURL) { + await PageThumbsStorage.copy(aFinalURL, aOriginalURL, aNoOverwrite); + Services.obs.notifyObservers(null, "page-thumbnail:create", aOriginalURL); + } + }, + + /** + * Register an expiration filter. + * + * When thumbnails are going to expire, each registered filter is asked for a + * list of thumbnails to keep. + * + * The filter (if it is a callable) or its filterForThumbnailExpiration method + * (if the filter is an object) is called with a single argument. The + * argument is a callback function. The filter must call the callback + * function and pass it an array of zero or more URLs. (It may do so + * asynchronously.) Thumbnails for those URLs will be except from expiration. + * + * @param aFilter callable, or object with filterForThumbnailExpiration method + */ + addExpirationFilter: function PageThumbs_addExpirationFilter(aFilter) { + PageThumbsExpiration.addFilter(aFilter); + }, + + /** + * Unregister an expiration filter. + * @param aFilter A filter that was previously passed to addExpirationFilter. + */ + removeExpirationFilter: function PageThumbs_removeExpirationFilter(aFilter) { + PageThumbsExpiration.removeFilter(aFilter); + }, + + /** + * Creates a new hidden canvas element. + * @param aWindow The document of this window will be used to create the + * canvas. If not given, the hidden window will be used. + * @return The newly created canvas. + */ + createCanvas: function PageThumbs_createCanvas(aWindow) { + return lazy.PageThumbUtils.createCanvas(aWindow); + }, + + _prefEnabled: function PageThumbs_prefEnabled() { + try { + return !Services.prefs.getBoolPref( + "browser.pagethumbnails.capturing_disabled" + ); + } catch (e) { + return true; + } + }, +}; + +export var PageThumbsStorage = { + ensurePath: function Storage_ensurePath() { + // Create the directory (ignore any error if the directory + // already exists). As all writes are done from the PageThumbsWorker + // thread, which serializes its operations, this ensures that + // future operations can proceed without having to check whether + // the directory exists. + return PageThumbsWorker.post("makeDir", [ + lazy.PageThumbsStorageService.path, + { ignoreExisting: true }, + ]).catch(function onError(aReason) { + console.error("Could not create thumbnails directory", aReason); + }); + }, + + _revisionTable: {}, + + // Generate an arbitrary revision tag, i.e. one that can't be used to + // infer URL frecency. + updateRevision(aURL) { + // Initialize with a random value and increment on each update. Wrap around + // modulo _revisionRange, so that even small values carry no meaning. + let rev = this._revisionTable[aURL]; + if (rev == null) { + rev = Math.floor(Math.random() * this._revisionRange); + } + this._revisionTable[aURL] = (rev + 1) % this._revisionRange; + }, + + // If two thumbnails with the same URL and revision are in cache at the + // same time, the image loader may pick the stale thumbnail in some cases. + // Therefore _revisionRange must be large enough to prevent this, e.g. + // in the pathological case image.cache.size (5MB by default) could fill + // with (abnormally small) 10KB thumbnail images if the browser session + // runs long enough (though this is unlikely as thumbnails are usually + // only updated every MAX_THUMBNAIL_AGE_SECS). + _revisionRange: 8192, + + /** + * Return a revision tag for the thumbnail stored for a given URL. + * + * @param aURL The URL spec string + * @return A revision tag for the corresponding thumbnail. Returns a changed + * value whenever the stored thumbnail changes. + */ + getRevision(aURL) { + let rev = this._revisionTable[aURL]; + if (rev == null) { + this.updateRevision(aURL); + rev = this._revisionTable[aURL]; + } + return rev; + }, + + /** + * Write the contents of a thumbnail, off the main thread. + * + * @param {string} aURL The url for which to store a thumbnail. + * @param {ArrayBuffer} aData The data to store in the thumbnail, as + * an ArrayBuffer. This array buffer will be detached and cannot be + * reused after the copy. + * @param {boolean} aNoOverwrite If true and the thumbnail's file already + * exists, the file will not be overwritten. + * + * @return {Promise} + */ + writeData: function Storage_writeData(aURL, aData, aNoOverwrite) { + let path = lazy.PageThumbsStorageService.getFilePathForURL(aURL); + this.ensurePath(); + aData = new Uint8Array(aData); + let msg = [ + path, + aData, + { + tmpPath: path + ".tmp", + mode: aNoOverwrite ? "create" : "overwrite", + }, + ]; + return PageThumbsWorker.post( + "writeAtomic", + msg, + msg /* we don't want that message garbage-collected, + as OS.Shared.Type.void_t.in_ptr.toMsg uses C-level + memory tricks to enforce zero-copy*/ + ).then( + () => this.updateRevision(aURL), + this._eatNoOverwriteError(aNoOverwrite) + ); + }, + + /** + * Copy a thumbnail, off the main thread. + * + * @param {string} aSourceURL The url of the thumbnail to copy. + * @param {string} aTargetURL The url of the target thumbnail. + * @param {boolean} aNoOverwrite If true and the target file already exists, + * the file will not be overwritten. + * + * @return {Promise} + */ + copy: function Storage_copy(aSourceURL, aTargetURL, aNoOverwrite) { + this.ensurePath(); + let sourceFile = + lazy.PageThumbsStorageService.getFilePathForURL(aSourceURL); + let targetFile = + lazy.PageThumbsStorageService.getFilePathForURL(aTargetURL); + let options = { noOverwrite: aNoOverwrite }; + return PageThumbsWorker.post("copy", [ + sourceFile, + targetFile, + options, + ]).then( + () => this.updateRevision(aTargetURL), + this._eatNoOverwriteError(aNoOverwrite) + ); + }, + + /** + * Remove a single thumbnail, off the main thread. + * + * @return {Promise} + */ + remove: function Storage_remove(aURL) { + return PageThumbsWorker.post("remove", [ + lazy.PageThumbsStorageService.getFilePathForURL(aURL), + ]); + }, + + /** + * Remove all thumbnails, off the main thread. + * + * @return {Promise} + */ + wipe: async function Storage_wipe() { + // + // This operation may be launched during shutdown, so we need to + // take a few precautions to ensure that: + // + // 1. it is not interrupted by shutdown, in which case we + // could be leaving privacy-sensitive files on disk; + // 2. it is not launched too late during shutdown, in which + // case this could cause shutdown freezes (see bug 1005487, + // which will eventually be fixed by bug 965309) + // + + let blocker = () => undefined; + + // The following operation will rise an error if we have already + // reached profileBeforeChange, in which case it is too late + // to clear the thumbnail wipe. + IOUtils.profileBeforeChange.addBlocker( + "PageThumbs: removing all thumbnails", + blocker + ); + + // Start the work only now that `profileBeforeChange` has had + // a chance to throw an error. + + let promise = PageThumbsWorker.post("wipe", [ + lazy.PageThumbsStorageService.path, + ]); + try { + await promise; + } finally { + // Generally, we will be done much before profileBeforeChange, + // so let's not hoard blockers. + IOUtils.profileBeforeChange.removeBlocker(blocker); + } + }, + + fileExistsForURL: function Storage_fileExistsForURL(aURL) { + return PageThumbsWorker.post("exists", [ + lazy.PageThumbsStorageService.getFilePathForURL(aURL), + ]); + }, + + isFileRecentForURL: function Storage_isFileRecentForURL(aURL) { + return PageThumbsWorker.post("isFileRecent", [ + lazy.PageThumbsStorageService.getFilePathForURL(aURL), + MAX_THUMBNAIL_AGE_SECS, + ]); + }, + + /** + * For functions that take a noOverwrite option, IOUtils throws an error if + * the target file exists and noOverwrite is true. We don't consider that an + * error, and we don't want such errors propagated. + * + * @param {aNoOverwrite} The noOverwrite option used in the IOUtils operation. + * + * @return {function} A function that should be passed as the second argument + * to then() (the `onError` argument). + */ + _eatNoOverwriteError: function Storage__eatNoOverwriteError(aNoOverwrite) { + return function onError(err) { + if ( + !aNoOverwrite || + !DOMException.isInstance(err) || + err.name !== "TypeMismatchError" + ) { + throw err; + } + }; + }, +}; + +var PageThumbsStorageMigrator = { + get currentVersion() { + try { + return Services.prefs.getIntPref(PREF_STORAGE_VERSION); + } catch (e) { + // The pref doesn't exist, yet. Return version 0. + return 0; + } + }, + + set currentVersion(aVersion) { + Services.prefs.setIntPref(PREF_STORAGE_VERSION, aVersion); + }, + + migrate: function Migrator_migrate() { + let version = this.currentVersion; + + // Storage version 1 never made it to beta. + // At the time of writing only Windows had (ProfD != ProfLD) and we + // needed to move thumbnails from the roaming profile to the locale + // one so that they're not needlessly included in backups and/or + // written via SMB. + + // Storage version 2 also never made it to beta. + // The thumbnail folder structure has been changed and old thumbnails + // were not migrated. Instead, we just renamed the current folder to + // "<name>-old" and will remove it later. + + if (version < 3) { + this.migrateToVersion3(); + } + + this.currentVersion = LATEST_STORAGE_VERSION; + }, + + /** + * Bug 239254 added support for having the disk cache and thumbnail + * directories on a local path (i.e. ~/.cache/) under Linux. We'll first + * try to move the old thumbnails to their new location. If that's not + * possible (because ProfD might be on a different file system than + * ProfLD) we'll just discard them. + * + * @param {string*} local The path to the local profile directory. + * Used for testing. Default argument is good for all non-testing uses. + * @param {string*} roaming The path to the roaming profile directory. + * Used for testing. Default argument is good for all non-testing uses. + */ + migrateToVersion3: function Migrator_migrateToVersion3( + local = Services.dirsvc.get("ProfLD", Ci.nsIFile).path, + roaming = Services.dirsvc.get("ProfD", Ci.nsIFile).path + ) { + PageThumbsWorker.post("moveOrDeleteAllThumbnails", [ + PathUtils.join(roaming, THUMBNAIL_DIRECTORY), + PathUtils.join(local, THUMBNAIL_DIRECTORY), + ]); + }, +}; + +// Export required for testing +export var PageThumbsExpiration = { + _filters: [], + + init: function Expiration_init() { + lazy.gUpdateTimerManager.registerTimer( + "browser-cleanup-thumbnails", + this, + EXPIRATION_INTERVAL_SECS + ); + }, + + addFilter: function Expiration_addFilter(aFilter) { + this._filters.push(aFilter); + }, + + removeFilter: function Expiration_removeFilter(aFilter) { + let index = this._filters.indexOf(aFilter); + if (index > -1) { + this._filters.splice(index, 1); + } + }, + + notify: function Expiration_notify(aTimer) { + let urls = []; + let filtersToWaitFor = this._filters.length; + + let expire = () => { + this.expireThumbnails(urls); + }; + + // No registered filters. + if (!filtersToWaitFor) { + expire(); + return; + } + + function filterCallback(aURLs) { + urls = urls.concat(aURLs); + if (--filtersToWaitFor == 0) { + expire(); + } + } + + for (let filter of this._filters) { + if (typeof filter == "function") { + filter(filterCallback); + } else { + filter.filterForThumbnailExpiration(filterCallback); + } + } + }, + + expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) { + let keep = aURLsToKeep.map(url => + lazy.PageThumbsStorageService.getLeafNameForURL(url) + ); + let msg = [ + lazy.PageThumbsStorageService.path, + keep, + EXPIRATION_MIN_CHUNK_SIZE, + ]; + + return PageThumbsWorker.post("expireFilesInDirectory", msg); + }, +}; + +/** + * Interface to a dedicated thread handling I/O + */ +var PageThumbsWorker = new BasePromiseWorker( + "resource://gre/modules/PageThumbs.worker.js" +); |