/* 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/. */ /** * Handles serialization of Download objects and persistence into a file, so * that the state of downloads can be restored across sessions. * * The file is stored in JSON format, without indentation. With indentation * applied, the file would look like this: * * { * "list": [ * { * "source": "http://www.example.com/download.txt", * "target": "/home/user/Downloads/download.txt" * }, * { * "source": { * "url": "http://www.example.com/download.txt", * "referrerInfo": serialized string represents referrerInfo object * }, * "target": "/home/user/Downloads/download-2.txt" * } * ] * } */ "use strict"; var EXPORTED_SYMBOLS = ["DownloadStore"]; const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); ChromeUtils.defineModuleGetter( this, "Downloads", "resource://gre/modules/Downloads.jsm" ); XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function() { return new TextDecoder(); }); XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function() { return new TextEncoder(); }); /** * Handles serialization of Download objects and persistence into a file, so * that the state of downloads can be restored across sessions. * * @param aList * DownloadList object to be populated or serialized. * @param aPath * String containing the file path where data should be saved. */ var DownloadStore = function(aList, aPath) { this.list = aList; this.path = aPath; }; DownloadStore.prototype = { /** * DownloadList object to be populated or serialized. */ list: null, /** * String containing the file path where data should be saved. */ path: "", /** * This function is called with a Download object as its first argument, and * should return true if the item should be saved. */ onsaveitem: () => true, /** * Loads persistent downloads from the file to the list. * * @return {Promise} * @resolves When the operation finished successfully. * @rejects JavaScript exception. */ load: function DS_load() { return (async () => { let bytes; try { bytes = await IOUtils.read(this.path); } catch (ex) { if (!(ex.name == "NotFoundError")) { throw ex; } // If the file does not exist, there are no downloads to load. return; } let storeData = JSON.parse(gTextDecoder.decode(bytes)); // Create live downloads based on the static snapshot. for (let downloadData of storeData.list) { try { let download = await Downloads.createDownload(downloadData); try { if (!download.succeeded && !download.canceled && !download.error) { // Try to restart the download if it was in progress during the // previous session. Ignore errors. download.start().catch(() => {}); } else { // If the download was not in progress, try to update the current // progress from disk. This is relevant in case we retained // partially downloaded data. await download.refresh(); } } finally { // Add the download to the list if we succeeded in creating it, // after we have updated its initial state. await this.list.add(download); } } catch (ex) { // If an item is unrecognized, don't prevent others from being loaded. Cu.reportError(ex); } } })(); }, /** * Saves persistent downloads from the list to the file. * * If an error occurs, the previous file is not deleted. * * @return {Promise} * @resolves When the operation finished successfully. * @rejects JavaScript exception. */ save: function DS_save() { return (async () => { let downloads = await this.list.getAll(); // Take a static snapshot of the current state of all the downloads. let storeData = { list: [] }; let atLeastOneDownload = false; for (let download of downloads) { try { if (!this.onsaveitem(download)) { continue; } let serializable = download.toSerializable(); if (!serializable) { // This item cannot be persisted across sessions. continue; } storeData.list.push(serializable); atLeastOneDownload = true; } catch (ex) { // If an item cannot be converted to a serializable form, don't // prevent others from being saved. Cu.reportError(ex); } } if (atLeastOneDownload) { // Create or overwrite the file if there are downloads to save. let bytes = gTextEncoder.encode(JSON.stringify(storeData)); await IOUtils.write(this.path, bytes, { tmpPath: this.path + ".tmp", }); } else { // Remove the file if there are no downloads to save at all. try { await IOUtils.remove(this.path); } catch (ex) { if (!(ex.name == "NotFoundError" || ex.name == "NotAllowedError")) { throw ex; } // On Windows, we may get an access denied error instead of a no such // file error if the file existed before, and was recently deleted. } } })(); }, };