diff options
Diffstat (limited to 'toolkit/components/jsdownloads/src/DownloadStore.jsm')
-rw-r--r-- | toolkit/components/jsdownloads/src/DownloadStore.jsm | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/toolkit/components/jsdownloads/src/DownloadStore.jsm b/toolkit/components/jsdownloads/src/DownloadStore.jsm new file mode 100644 index 000000000..765a45c5a --- /dev/null +++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm @@ -0,0 +1,203 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* 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", + * "referrer": "http://www.example.com/referrer.html" + * }, + * "target": "/home/user/Downloads/download-2.txt" + * } + * ] + * } + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DownloadStore", +]; + +// Globals + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm") +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () { + return new TextDecoder(); +}); + +XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () { + return new TextEncoder(); +}); + +// DownloadStore + +/** + * 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. + */ +this.DownloadStore = function (aList, aPath) +{ + this.list = aList; + this.path = aPath; +} + +this.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 Task.spawn(function* task_DS_load() { + let bytes; + try { + bytes = yield OS.File.read(this.path); + } catch (ex) { + if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) { + 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 = yield 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. + yield download.refresh(); + } + } finally { + // Add the download to the list if we succeeded in creating it, + // after we have updated its initial state. + yield this.list.add(download); + } + } catch (ex) { + // If an item is unrecognized, don't prevent others from being loaded. + Cu.reportError(ex); + } + } + }.bind(this)); + }, + + /** + * 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 Task.spawn(function* task_DS_save() { + let downloads = yield 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)); + yield OS.File.writeAtomic(this.path, bytes, + { tmpPath: this.path + ".tmp" }); + } else { + // Remove the file if there are no downloads to save at all. + try { + yield OS.File.remove(this.path); + } catch (ex) { + if (!(ex instanceof OS.File.Error) || + !(ex.becauseNoSuchFile || ex.becauseAccessDenied)) { + 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. + } + } + }.bind(this)); + }, +}; |