michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Handles serialization of Download objects and persistence into a file, so michael@0: * that the state of downloads can be restored across sessions. michael@0: * michael@0: * The file is stored in JSON format, without indentation. With indentation michael@0: * applied, the file would look like this: michael@0: * michael@0: * { michael@0: * "list": [ michael@0: * { michael@0: * "source": "http://www.example.com/download.txt", michael@0: * "target": "/home/user/Downloads/download.txt" michael@0: * }, michael@0: * { michael@0: * "source": { michael@0: * "url": "http://www.example.com/download.txt", michael@0: * "referrer": "http://www.example.com/referrer.html" michael@0: * }, michael@0: * "target": "/home/user/Downloads/download-2.txt" michael@0: * } michael@0: * ] michael@0: * } michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "DownloadStore", michael@0: ]; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Downloads", michael@0: "resource://gre/modules/Downloads.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm") michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () { michael@0: return new TextDecoder(); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () { michael@0: return new TextEncoder(); michael@0: }); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadStore michael@0: michael@0: /** michael@0: * Handles serialization of Download objects and persistence into a file, so michael@0: * that the state of downloads can be restored across sessions. michael@0: * michael@0: * @param aList michael@0: * DownloadList object to be populated or serialized. michael@0: * @param aPath michael@0: * String containing the file path where data should be saved. michael@0: */ michael@0: this.DownloadStore = function (aList, aPath) michael@0: { michael@0: this.list = aList; michael@0: this.path = aPath; michael@0: } michael@0: michael@0: this.DownloadStore.prototype = { michael@0: /** michael@0: * DownloadList object to be populated or serialized. michael@0: */ michael@0: list: null, michael@0: michael@0: /** michael@0: * String containing the file path where data should be saved. michael@0: */ michael@0: path: "", michael@0: michael@0: /** michael@0: * This function is called with a Download object as its first argument, and michael@0: * should return true if the item should be saved. michael@0: */ michael@0: onsaveitem: () => true, michael@0: michael@0: /** michael@0: * Loads persistent downloads from the file to the list. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the operation finished successfully. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: load: function DS_load() michael@0: { michael@0: return Task.spawn(function task_DS_load() { michael@0: let bytes; michael@0: try { michael@0: bytes = yield OS.File.read(this.path); michael@0: } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { michael@0: // If the file does not exist, there are no downloads to load. michael@0: return; michael@0: } michael@0: michael@0: let storeData = JSON.parse(gTextDecoder.decode(bytes)); michael@0: michael@0: // Create live downloads based on the static snapshot. michael@0: for (let downloadData of storeData.list) { michael@0: try { michael@0: let download = yield Downloads.createDownload(downloadData); michael@0: try { michael@0: if (!download.succeeded && !download.canceled && !download.error) { michael@0: // Try to restart the download if it was in progress during the michael@0: // previous session. michael@0: download.start(); michael@0: } else { michael@0: // If the download was not in progress, try to update the current michael@0: // progress from disk. This is relevant in case we retained michael@0: // partially downloaded data. michael@0: yield download.refresh(); michael@0: } michael@0: } finally { michael@0: // Add the download to the list if we succeeded in creating it, michael@0: // after we have updated its initial state. michael@0: yield this.list.add(download); michael@0: } michael@0: } catch (ex) { michael@0: // If an item is unrecognized, don't prevent others from being loaded. michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Saves persistent downloads from the list to the file. michael@0: * michael@0: * If an error occurs, the previous file is not deleted. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the operation finished successfully. michael@0: * @rejects JavaScript exception. michael@0: */ michael@0: save: function DS_save() michael@0: { michael@0: return Task.spawn(function task_DS_save() { michael@0: let downloads = yield this.list.getAll(); michael@0: michael@0: // Take a static snapshot of the current state of all the downloads. michael@0: let storeData = { list: [] }; michael@0: let atLeastOneDownload = false; michael@0: for (let download of downloads) { michael@0: try { michael@0: if (!this.onsaveitem(download)) { michael@0: continue; michael@0: } michael@0: storeData.list.push(download.toSerializable()); michael@0: atLeastOneDownload = true; michael@0: } catch (ex) { michael@0: // If an item cannot be converted to a serializable form, don't michael@0: // prevent others from being saved. michael@0: Cu.reportError(ex); michael@0: } michael@0: } michael@0: michael@0: if (atLeastOneDownload) { michael@0: // Create or overwrite the file if there are downloads to save. michael@0: let bytes = gTextEncoder.encode(JSON.stringify(storeData)); michael@0: yield OS.File.writeAtomic(this.path, bytes, michael@0: { tmpPath: this.path + ".tmp" }); michael@0: } else { michael@0: // Remove the file if there are no downloads to save at all. michael@0: try { michael@0: yield OS.File.remove(this.path); michael@0: } catch (ex if ex instanceof OS.File.Error && michael@0: (ex.becauseNoSuchFile || ex.becauseAccessDenied)) { michael@0: // On Windows, we may get an access denied error instead of a no such michael@0: // file error if the file existed before, and was recently deleted. michael@0: } michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: };