1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var EXPORTED_SYMBOLS = ["PageThumbs", "PageThumbsStorage"];
9 const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version";
10 const LATEST_STORAGE_VERSION = 3;
12 const EXPIRATION_MIN_CHUNK_SIZE = 50;
13 const EXPIRATION_INTERVAL_SECS = 3600;
15 // If a request for a thumbnail comes in and we find one that is "stale"
16 // (or don't find one at all) we automatically queue a request to generate a
18 const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs.
21 * Name of the directory in the profile that contains the thumbnails.
23 const THUMBNAIL_DIRECTORY = "thumbnails";
25 const { XPCOMUtils } = ChromeUtils.importESModule(
26 "resource://gre/modules/XPCOMUtils.sys.mjs"
28 const { BasePromiseWorker } = ChromeUtils.import(
29 "resource://gre/modules/PromiseWorker.jsm"
34 XPCOMUtils.defineLazyModuleGetters(lazy, {
35 PageThumbUtils: "resource://gre/modules/PageThumbUtils.jsm",
36 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
39 XPCOMUtils.defineLazyServiceGetter(
41 "gUpdateTimerManager",
42 "@mozilla.org/updates/timer-manager;1",
43 "nsIUpdateTimerManager"
46 XPCOMUtils.defineLazyServiceGetter(
48 "PageThumbsStorageService",
49 "@mozilla.org/thumbnails/pagethumbs-service;1",
50 "nsIPageThumbsStorageService"
54 * Utilities for dealing with promises.
58 * Read the bytes from a blob, asynchronously.
61 * @resolve {ArrayBuffer} In case of success, the bytes contained in the blob.
62 * @reject {DOMException} In case of error, the underlying DOMException.
64 readBlob: function readBlob(blob) {
65 return new Promise((resolve, reject) => {
66 let reader = new FileReader();
67 reader.onloadend = function onloadend() {
68 if (reader.readyState != FileReader.DONE) {
71 resolve(reader.result);
74 reader.readAsArrayBuffer(blob);
80 * Singleton providing functionality for capturing web page thumbnails and for
81 * accessing them if already cached.
87 * The calculated width and height of the thumbnails.
93 * The scheme to use for thumbnail urls.
96 return "moz-page-thumb";
100 * The static host to use for thumbnail urls.
107 * The thumbnails' image type.
113 init: function PageThumbs_init() {
114 if (!this._initialized) {
115 this._initialized = true;
117 this._placesObserver = new PlacesWeakCallbackWrapper(
118 this.handlePlacesEvents.bind(this)
120 PlacesObservers.addListener(
121 ["history-cleared", "page-removed"],
125 // Migrate the underlying storage, if needed.
126 PageThumbsStorageMigrator.migrate();
127 PageThumbsExpiration.init();
131 handlePlacesEvents(events) {
132 for (const event of events) {
133 switch (event.type) {
134 case "history-cleared": {
135 PageThumbsStorage.wipe();
138 case "page-removed": {
139 if (event.isRemovedFromStore) {
140 PageThumbsStorage.remove(event.url);
148 uninit: function PageThumbs_uninit() {
149 if (this._initialized) {
150 this._initialized = false;
155 * Gets the thumbnail image's url for a given web page's url.
156 * @param aUrl The web page's url that is depicted in the thumbnail.
157 * @return The thumbnail image's url.
159 getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) {
165 encodeURIComponent(aUrl) +
167 PageThumbsStorage.getRevision(aUrl)
172 * Gets the path of the thumbnail file for a given web page's
173 * url. This file may or may not exist depending on whether the
174 * thumbnail has been captured or not.
176 * @param aUrl The web page's url.
177 * @return The path of the thumbnail file.
179 getThumbnailPath: function PageThumbs_getThumbnailPath(aUrl) {
180 return lazy.PageThumbsStorageService.getFilePathForURL(aUrl);
184 * Asynchronously returns a thumbnail as a blob for the given
187 * @param aBrowser The <browser> to capture a thumbnail from.
188 * @param aArgs See captureToCanvas for accepted arguments.
190 * @resolve {Blob} The thumbnail, as a Blob.
192 captureToBlob: function PageThumbs_captureToBlob(aBrowser, aArgs) {
193 if (!this._prefEnabled()) {
197 return new Promise(resolve => {
198 let canvas = this.createCanvas(aBrowser.ownerGlobal);
199 this.captureToCanvas(aBrowser, canvas, aArgs)
201 canvas.toBlob(blob => {
202 resolve(blob, this.contentType);
205 .catch(e => Cu.reportError(e));
210 * Captures a thumbnail from a given window and draws it to the given canvas.
211 * Note, when dealing with remote content, this api draws into the passed
212 * canvas asynchronously. Pass aCallback to receive an async callback after
213 * canvas painting has completed.
214 * @param aBrowser The browser to capture a thumbnail from.
215 * @param aCanvas The canvas to draw to. The thumbnail will be scaled to match
216 * the dimensions of this canvas. If callers pass a 0x0 canvas, the canvas
217 * will be resized to default thumbnail dimensions just prior to painting.
218 * @param aArgs (optional) Additional named parameters:
219 * fullScale - request that a non-downscaled image be returned.
220 * isImage - indicate that this should be treated as an image url.
221 * backgroundColor - background color to draw behind images.
222 * targetWidth - desired width for images.
223 * isBackgroundThumb - true if request is from the background thumb service.
224 * fullViewport - request that a screenshot for the viewport be
225 * captured. This makes it possible to get a screenshot that reflects
226 * the current scroll position of aBrowser.
227 * @param aSkipTelemetry skip recording telemetry
229 async captureToCanvas(aBrowser, aCanvas, aArgs, aSkipTelemetry = false) {
230 let telemetryCaptureTime = new Date();
232 fullScale: aArgs ? aArgs.fullScale : false,
233 isImage: aArgs ? aArgs.isImage : false,
235 aArgs?.backgroundColor ?? lazy.PageThumbUtils.THUMBNAIL_BG_COLOR,
237 aArgs?.targetWidth ?? lazy.PageThumbUtils.THUMBNAIL_DEFAULT_SIZE,
238 isBackgroundThumb: aArgs ? aArgs.isBackgroundThumb : false,
239 fullViewport: aArgs?.fullViewport ?? false,
242 return this._captureToCanvas(aBrowser, aCanvas, args).then(() => {
243 if (!aSkipTelemetry) {
245 .getHistogramById("FX_THUMBNAILS_CAPTURE_TIME_MS")
246 .add(new Date() - telemetryCaptureTime);
253 * Asynchronously check the state of aBrowser to see if it passes a set of
254 * predefined security checks. Consumers should refrain from storing
255 * thumbnails if these checks fail. Note the final result of this call is
256 * transitory as it is based on current navigation state and the type of
257 * content being displayed.
259 * @param aBrowser The target browser
261 async shouldStoreThumbnail(aBrowser) {
262 // Don't capture in private browsing mode.
263 if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
266 if (aBrowser.isRemoteBrowser) {
267 if (aBrowser.browsingContext.currentWindowGlobal) {
268 let thumbnailsActor = aBrowser.browsingContext.currentWindowGlobal.getActor(
271 return thumbnailsActor
272 .sendQuery("Browser:Thumbnail:CheckState")
279 return lazy.PageThumbUtils.shouldStoreContentThumbnail(
280 aBrowser.contentDocument,
285 // The background thumbnail service captures to canvas but doesn't want to
286 // participate in this service's telemetry, which is why this method exists.
287 async _captureToCanvas(aBrowser, aCanvas, aArgs) {
288 if (aBrowser.isRemoteBrowser) {
289 let thumbnail = await this._captureRemoteThumbnail(
296 // 'thumbnail' can be null if the browser has navigated away after starting
297 // the thumbnail request, so we check it here.
299 let ctx = thumbnail.getContext("2d");
300 let imgData = ctx.getImageData(0, 0, thumbnail.width, thumbnail.height);
301 aCanvas.width = thumbnail.width;
302 aCanvas.height = thumbnail.height;
303 aCanvas.getContext("2d").putImageData(imgData, 0, 0);
308 // The content is a local page, grab a thumbnail sync.
309 await lazy.PageThumbUtils.createSnapshotThumbnail(aBrowser, aCanvas, aArgs);
314 * Asynchrnously render an appropriately scaled thumbnail to canvas.
316 * @param aBrowser The browser to capture a thumbnail from.
317 * @param aWidth The desired canvas width.
318 * @param aHeight The desired canvas height.
319 * @param aArgs (optional) Additional named parameters:
320 * fullScale - request that a non-downscaled image be returned.
321 * isImage - indicate that this should be treated as an image url.
322 * backgroundColor - background color to draw behind images.
323 * targetWidth - desired width for images.
324 * isBackgroundThumb - true if request is from the background thumb service.
325 * fullViewport - request that a screenshot for the viewport be
326 * captured. This makes it possible to get a screenshot that reflects
327 * the current scroll position of aBrowser.
330 async _captureRemoteThumbnail(aBrowser, aWidth, aHeight, aArgs) {
331 if (!aBrowser.browsingContext || !aBrowser.parentElement) {
335 let thumbnailsActor = aBrowser.browsingContext.currentWindowGlobal.getActor(
336 aArgs.isBackgroundThumb ? "BackgroundThumbnails" : "Thumbnails"
338 let contentInfo = await thumbnailsActor.sendQuery(
339 "Browser:Thumbnail:ContentInfo",
341 isImage: aArgs.isImage,
342 targetWidth: aArgs.targetWidth,
343 backgroundColor: aArgs.backgroundColor,
347 let contentWidth = contentInfo.width;
348 let contentHeight = contentInfo.height;
349 if (contentWidth == 0 || contentHeight == 0) {
350 throw new Error("IMAGE_ZERO_DIMENSION");
353 let doc = aBrowser.parentElement.ownerDocument;
354 let thumbnail = doc.createElementNS(
355 lazy.PageThumbUtils.HTML_NAMESPACE,
360 if (contentInfo.imageData) {
361 thumbnail.width = contentWidth;
362 thumbnail.height = contentHeight;
364 image = new aBrowser.ownerGlobal.Image();
365 await new Promise(resolve => {
366 image.onload = resolve;
367 image.src = contentInfo.imageData;
370 let fullScale = aArgs ? aArgs.fullScale : false;
371 let scale = fullScale
373 : Math.min(Math.max(aWidth / contentWidth, aHeight / contentHeight), 1);
375 image = await aBrowser.drawSnapshot(
381 aArgs.backgroundColor,
385 thumbnail.width = fullScale ? contentWidth : aWidth;
386 thumbnail.height = fullScale ? contentHeight : aHeight;
389 thumbnail.getContext("2d").drawImage(image, 0, 0);
395 * Captures a thumbnail for the given browser and stores it to the cache.
396 * @param aBrowser The browser to capture a thumbnail for.
398 captureAndStore: async function PageThumbs_captureAndStore(aBrowser) {
399 if (!this._prefEnabled()) {
403 let url = aBrowser.currentURI.spec;
405 let channelError = false;
407 if (!aBrowser.isRemoteBrowser) {
408 let channel = aBrowser.docShell.currentDocumentChannel;
409 originalURL = channel.originalURI.spec;
410 // see if this was an error response.
411 channelError = lazy.PageThumbUtils.isChannelErrorResponse(channel);
413 let thumbnailsActor = aBrowser.browsingContext.currentWindowGlobal.getActor(
416 let resp = await thumbnailsActor.sendQuery(
417 "Browser:Thumbnail:GetOriginalURL"
420 originalURL = resp.originalURL || url;
421 channelError = resp.channelError;
425 let blob = await this.captureToBlob(aBrowser);
426 let buffer = await TaskUtils.readBlob(blob);
427 await this._store(originalURL, url, buffer, channelError);
429 Cu.reportError("Exception thrown during thumbnail capture: '" + ex + "'");
434 * Checks if an existing thumbnail for the specified URL is either missing
435 * or stale, and if so, captures and stores it. Once the thumbnail is stored,
436 * an observer service notification will be sent, so consumers should observe
437 * such notifications if they want to be notified of an updated thumbnail.
439 * @param aBrowser The content window of this browser will be captured.
441 captureAndStoreIfStale: async function PageThumbs_captureAndStoreIfStale(
444 if (!aBrowser.currentURI) {
447 let url = aBrowser.currentURI.spec;
450 recent = await PageThumbsStorage.isFileRecentForURL(url);
456 // Careful, the call to PageThumbsStorage is async, so the browser may
457 // have navigated away from the URL or even closed.
458 aBrowser.currentURI &&
459 aBrowser.currentURI.spec == url
461 await this.captureAndStore(aBrowser);
467 * Stores data to disk for the given URLs.
469 * NB: The background thumbnail service calls this, too.
471 * @param aOriginalURL The URL with which the capture was initiated.
472 * @param aFinalURL The URL to which aOriginalURL ultimately resolved.
473 * @param aData An ArrayBuffer containing the image data.
474 * @param aNoOverwrite If true and files for the URLs already exist, the files
475 * will not be overwritten.
477 _store: async function PageThumbs__store(
483 let telemetryStoreTime = new Date();
484 await PageThumbsStorage.writeData(aFinalURL, aData, aNoOverwrite);
486 .getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
487 .add(new Date() - telemetryStoreTime);
489 Services.obs.notifyObservers(null, "page-thumbnail:create", aFinalURL);
490 // We've been redirected. Create a copy of the current thumbnail for
491 // the redirect source. We need to do this because:
493 // 1) Users can drag any kind of links onto the newtab page. If those
494 // links redirect to a different URL then we want to be able to
495 // provide thumbnails for both of them.
497 // 2) The newtab page should actually display redirect targets, only.
498 // Because of bug 559175 this information can get lost when using
499 // Sync and therefore also redirect sources appear on the newtab
500 // page. We also want thumbnails for those.
501 if (aFinalURL != aOriginalURL) {
502 await PageThumbsStorage.copy(aFinalURL, aOriginalURL, aNoOverwrite);
503 Services.obs.notifyObservers(null, "page-thumbnail:create", aOriginalURL);
508 * Register an expiration filter.
510 * When thumbnails are going to expire, each registered filter is asked for a
511 * list of thumbnails to keep.
513 * The filter (if it is a callable) or its filterForThumbnailExpiration method
514 * (if the filter is an object) is called with a single argument. The
515 * argument is a callback function. The filter must call the callback
516 * function and pass it an array of zero or more URLs. (It may do so
517 * asynchronously.) Thumbnails for those URLs will be except from expiration.
519 * @param aFilter callable, or object with filterForThumbnailExpiration method
521 addExpirationFilter: function PageThumbs_addExpirationFilter(aFilter) {
522 PageThumbsExpiration.addFilter(aFilter);
526 * Unregister an expiration filter.
527 * @param aFilter A filter that was previously passed to addExpirationFilter.
529 removeExpirationFilter: function PageThumbs_removeExpirationFilter(aFilter) {
530 PageThumbsExpiration.removeFilter(aFilter);
534 * Creates a new hidden canvas element.
535 * @param aWindow The document of this window will be used to create the
536 * canvas. If not given, the hidden window will be used.
537 * @return The newly created canvas.
539 createCanvas: function PageThumbs_createCanvas(aWindow) {
540 return lazy.PageThumbUtils.createCanvas(aWindow);
543 _prefEnabled: function PageThumbs_prefEnabled() {
545 return !Services.prefs.getBoolPref(
546 "browser.pagethumbnails.capturing_disabled"
554 var PageThumbsStorage = {
555 ensurePath: function Storage_ensurePath() {
556 // Create the directory (ignore any error if the directory
557 // already exists). As all writes are done from the PageThumbsWorker
558 // thread, which serializes its operations, this ensures that
559 // future operations can proceed without having to check whether
560 // the directory exists.
561 return PageThumbsWorker.post("makeDir", [
562 lazy.PageThumbsStorageService.path,
563 { ignoreExisting: true },
564 ]).catch(function onError(aReason) {
565 Cu.reportError("Could not create thumbnails directory" + aReason);
571 // Generate an arbitrary revision tag, i.e. one that can't be used to
572 // infer URL frecency.
573 updateRevision(aURL) {
574 // Initialize with a random value and increment on each update. Wrap around
575 // modulo _revisionRange, so that even small values carry no meaning.
576 let rev = this._revisionTable[aURL];
578 rev = Math.floor(Math.random() * this._revisionRange);
580 this._revisionTable[aURL] = (rev + 1) % this._revisionRange;
583 // If two thumbnails with the same URL and revision are in cache at the
584 // same time, the image loader may pick the stale thumbnail in some cases.
585 // Therefore _revisionRange must be large enough to prevent this, e.g.
586 // in the pathological case image.cache.size (5MB by default) could fill
587 // with (abnormally small) 10KB thumbnail images if the browser session
588 // runs long enough (though this is unlikely as thumbnails are usually
589 // only updated every MAX_THUMBNAIL_AGE_SECS).
590 _revisionRange: 8192,
593 * Return a revision tag for the thumbnail stored for a given URL.
595 * @param aURL The URL spec string
596 * @return A revision tag for the corresponding thumbnail. Returns a changed
597 * value whenever the stored thumbnail changes.
600 let rev = this._revisionTable[aURL];
602 this.updateRevision(aURL);
603 rev = this._revisionTable[aURL];
609 * Write the contents of a thumbnail, off the main thread.
611 * @param {string} aURL The url for which to store a thumbnail.
612 * @param {ArrayBuffer} aData The data to store in the thumbnail, as
613 * an ArrayBuffer. This array buffer will be detached and cannot be
614 * reused after the copy.
615 * @param {boolean} aNoOverwrite If true and the thumbnail's file already
616 * exists, the file will not be overwritten.
620 writeData: function Storage_writeData(aURL, aData, aNoOverwrite) {
621 let path = lazy.PageThumbsStorageService.getFilePathForURL(aURL);
623 aData = new Uint8Array(aData);
628 tmpPath: path + ".tmp",
629 mode: aNoOverwrite ? "create" : "overwrite",
632 return PageThumbsWorker.post(
635 msg /* we don't want that message garbage-collected,
636 as OS.Shared.Type.void_t.in_ptr.toMsg uses C-level
637 memory tricks to enforce zero-copy*/
639 () => this.updateRevision(aURL),
640 this._eatNoOverwriteError(aNoOverwrite)
645 * Copy a thumbnail, off the main thread.
647 * @param {string} aSourceURL The url of the thumbnail to copy.
648 * @param {string} aTargetURL The url of the target thumbnail.
649 * @param {boolean} aNoOverwrite If true and the target file already exists,
650 * the file will not be overwritten.
654 copy: function Storage_copy(aSourceURL, aTargetURL, aNoOverwrite) {
656 let sourceFile = lazy.PageThumbsStorageService.getFilePathForURL(
659 let targetFile = lazy.PageThumbsStorageService.getFilePathForURL(
662 let options = { noOverwrite: aNoOverwrite };
663 return PageThumbsWorker.post("copy", [
668 () => this.updateRevision(aTargetURL),
669 this._eatNoOverwriteError(aNoOverwrite)
674 * Remove a single thumbnail, off the main thread.
678 remove: function Storage_remove(aURL) {
679 return PageThumbsWorker.post("remove", [
680 lazy.PageThumbsStorageService.getFilePathForURL(aURL),
685 * Remove all thumbnails, off the main thread.
689 wipe: async function Storage_wipe() {
691 // This operation may be launched during shutdown, so we need to
692 // take a few precautions to ensure that:
694 // 1. it is not interrupted by shutdown, in which case we
695 // could be leaving privacy-sensitive files on disk;
696 // 2. it is not launched too late during shutdown, in which
697 // case this could cause shutdown freezes (see bug 1005487,
698 // which will eventually be fixed by bug 965309)
701 let blocker = () => undefined;
703 // The following operation will rise an error if we have already
704 // reached profileBeforeChange, in which case it is too late
705 // to clear the thumbnail wipe.
706 IOUtils.profileBeforeChange.addBlocker(
707 "PageThumbs: removing all thumbnails",
711 // Start the work only now that `profileBeforeChange` has had
712 // a chance to throw an error.
714 let promise = PageThumbsWorker.post("wipe", [
715 lazy.PageThumbsStorageService.path,
720 // Generally, we will be done much before profileBeforeChange,
721 // so let's not hoard blockers.
722 IOUtils.profileBeforeChange.removeBlocker(blocker);
726 fileExistsForURL: function Storage_fileExistsForURL(aURL) {
727 return PageThumbsWorker.post("exists", [
728 lazy.PageThumbsStorageService.getFilePathForURL(aURL),
732 isFileRecentForURL: function Storage_isFileRecentForURL(aURL) {
733 return PageThumbsWorker.post("isFileRecent", [
734 lazy.PageThumbsStorageService.getFilePathForURL(aURL),
735 MAX_THUMBNAIL_AGE_SECS,
740 * For functions that take a noOverwrite option, IOUtils throws an error if
741 * the target file exists and noOverwrite is true. We don't consider that an
742 * error, and we don't want such errors propagated.
744 * @param {aNoOverwrite} The noOverwrite option used in the IOUtils operation.
746 * @return {function} A function that should be passed as the second argument
747 * to then() (the `onError` argument).
749 _eatNoOverwriteError: function Storage__eatNoOverwriteError(aNoOverwrite) {
750 return function onError(err) {
753 !DOMException.isInstance(err) ||
754 err.name !== "TypeMismatchError"
762 var PageThumbsStorageMigrator = {
763 get currentVersion() {
765 return Services.prefs.getIntPref(PREF_STORAGE_VERSION);
767 // The pref doesn't exist, yet. Return version 0.
772 set currentVersion(aVersion) {
773 Services.prefs.setIntPref(PREF_STORAGE_VERSION, aVersion);
776 migrate: function Migrator_migrate() {
777 let version = this.currentVersion;
779 // Storage version 1 never made it to beta.
780 // At the time of writing only Windows had (ProfD != ProfLD) and we
781 // needed to move thumbnails from the roaming profile to the locale
782 // one so that they're not needlessly included in backups and/or
785 // Storage version 2 also never made it to beta.
786 // The thumbnail folder structure has been changed and old thumbnails
787 // were not migrated. Instead, we just renamed the current folder to
788 // "<name>-old" and will remove it later.
791 this.migrateToVersion3();
794 this.currentVersion = LATEST_STORAGE_VERSION;
798 * Bug 239254 added support for having the disk cache and thumbnail
799 * directories on a local path (i.e. ~/.cache/) under Linux. We'll first
800 * try to move the old thumbnails to their new location. If that's not
801 * possible (because ProfD might be on a different file system than
802 * ProfLD) we'll just discard them.
804 * @param {string*} local The path to the local profile directory.
805 * Used for testing. Default argument is good for all non-testing uses.
806 * @param {string*} roaming The path to the roaming profile directory.
807 * Used for testing. Default argument is good for all non-testing uses.
809 migrateToVersion3: function Migrator_migrateToVersion3(
810 local = Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
811 roaming = Services.dirsvc.get("ProfD", Ci.nsIFile).path
813 PageThumbsWorker.post("moveOrDeleteAllThumbnails", [
814 PathUtils.join(roaming, THUMBNAIL_DIRECTORY),
815 PathUtils.join(local, THUMBNAIL_DIRECTORY),
820 var PageThumbsExpiration = {
823 init: function Expiration_init() {
824 lazy.gUpdateTimerManager.registerTimer(
825 "browser-cleanup-thumbnails",
827 EXPIRATION_INTERVAL_SECS
831 addFilter: function Expiration_addFilter(aFilter) {
832 this._filters.push(aFilter);
835 removeFilter: function Expiration_removeFilter(aFilter) {
836 let index = this._filters.indexOf(aFilter);
838 this._filters.splice(index, 1);
842 notify: function Expiration_notify(aTimer) {
844 let filtersToWaitFor = this._filters.length;
847 this.expireThumbnails(urls);
850 // No registered filters.
851 if (!filtersToWaitFor) {
856 function filterCallback(aURLs) {
857 urls = urls.concat(aURLs);
858 if (--filtersToWaitFor == 0) {
863 for (let filter of this._filters) {
864 if (typeof filter == "function") {
865 filter(filterCallback);
867 filter.filterForThumbnailExpiration(filterCallback);
872 expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
873 let keep = aURLsToKeep.map(url =>
874 lazy.PageThumbsStorageService.getLeafNameForURL(url)
877 lazy.PageThumbsStorageService.path,
879 EXPIRATION_MIN_CHUNK_SIZE,
882 return PageThumbsWorker.post("expireFilesInDirectory", msg);
887 * Interface to a dedicated thread handling I/O
889 var PageThumbsWorker = new BasePromiseWorker(
890 "resource://gre/modules/PageThumbsWorker.js"