1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,193 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +/** 1.11 + * Handles serialization of Download objects and persistence into a file, so 1.12 + * that the state of downloads can be restored across sessions. 1.13 + * 1.14 + * The file is stored in JSON format, without indentation. With indentation 1.15 + * applied, the file would look like this: 1.16 + * 1.17 + * { 1.18 + * "list": [ 1.19 + * { 1.20 + * "source": "http://www.example.com/download.txt", 1.21 + * "target": "/home/user/Downloads/download.txt" 1.22 + * }, 1.23 + * { 1.24 + * "source": { 1.25 + * "url": "http://www.example.com/download.txt", 1.26 + * "referrer": "http://www.example.com/referrer.html" 1.27 + * }, 1.28 + * "target": "/home/user/Downloads/download-2.txt" 1.29 + * } 1.30 + * ] 1.31 + * } 1.32 + */ 1.33 + 1.34 +"use strict"; 1.35 + 1.36 +this.EXPORTED_SYMBOLS = [ 1.37 + "DownloadStore", 1.38 +]; 1.39 + 1.40 +//////////////////////////////////////////////////////////////////////////////// 1.41 +//// Globals 1.42 + 1.43 +const Cc = Components.classes; 1.44 +const Ci = Components.interfaces; 1.45 +const Cu = Components.utils; 1.46 +const Cr = Components.results; 1.47 + 1.48 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.49 + 1.50 +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", 1.51 + "resource://gre/modules/Downloads.jsm"); 1.52 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.53 + "resource://gre/modules/osfile.jsm") 1.54 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.55 + "resource://gre/modules/Task.jsm"); 1.56 + 1.57 +XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () { 1.58 + return new TextDecoder(); 1.59 +}); 1.60 + 1.61 +XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () { 1.62 + return new TextEncoder(); 1.63 +}); 1.64 + 1.65 +//////////////////////////////////////////////////////////////////////////////// 1.66 +//// DownloadStore 1.67 + 1.68 +/** 1.69 + * Handles serialization of Download objects and persistence into a file, so 1.70 + * that the state of downloads can be restored across sessions. 1.71 + * 1.72 + * @param aList 1.73 + * DownloadList object to be populated or serialized. 1.74 + * @param aPath 1.75 + * String containing the file path where data should be saved. 1.76 + */ 1.77 +this.DownloadStore = function (aList, aPath) 1.78 +{ 1.79 + this.list = aList; 1.80 + this.path = aPath; 1.81 +} 1.82 + 1.83 +this.DownloadStore.prototype = { 1.84 + /** 1.85 + * DownloadList object to be populated or serialized. 1.86 + */ 1.87 + list: null, 1.88 + 1.89 + /** 1.90 + * String containing the file path where data should be saved. 1.91 + */ 1.92 + path: "", 1.93 + 1.94 + /** 1.95 + * This function is called with a Download object as its first argument, and 1.96 + * should return true if the item should be saved. 1.97 + */ 1.98 + onsaveitem: () => true, 1.99 + 1.100 + /** 1.101 + * Loads persistent downloads from the file to the list. 1.102 + * 1.103 + * @return {Promise} 1.104 + * @resolves When the operation finished successfully. 1.105 + * @rejects JavaScript exception. 1.106 + */ 1.107 + load: function DS_load() 1.108 + { 1.109 + return Task.spawn(function task_DS_load() { 1.110 + let bytes; 1.111 + try { 1.112 + bytes = yield OS.File.read(this.path); 1.113 + } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { 1.114 + // If the file does not exist, there are no downloads to load. 1.115 + return; 1.116 + } 1.117 + 1.118 + let storeData = JSON.parse(gTextDecoder.decode(bytes)); 1.119 + 1.120 + // Create live downloads based on the static snapshot. 1.121 + for (let downloadData of storeData.list) { 1.122 + try { 1.123 + let download = yield Downloads.createDownload(downloadData); 1.124 + try { 1.125 + if (!download.succeeded && !download.canceled && !download.error) { 1.126 + // Try to restart the download if it was in progress during the 1.127 + // previous session. 1.128 + download.start(); 1.129 + } else { 1.130 + // If the download was not in progress, try to update the current 1.131 + // progress from disk. This is relevant in case we retained 1.132 + // partially downloaded data. 1.133 + yield download.refresh(); 1.134 + } 1.135 + } finally { 1.136 + // Add the download to the list if we succeeded in creating it, 1.137 + // after we have updated its initial state. 1.138 + yield this.list.add(download); 1.139 + } 1.140 + } catch (ex) { 1.141 + // If an item is unrecognized, don't prevent others from being loaded. 1.142 + Cu.reportError(ex); 1.143 + } 1.144 + } 1.145 + }.bind(this)); 1.146 + }, 1.147 + 1.148 + /** 1.149 + * Saves persistent downloads from the list to the file. 1.150 + * 1.151 + * If an error occurs, the previous file is not deleted. 1.152 + * 1.153 + * @return {Promise} 1.154 + * @resolves When the operation finished successfully. 1.155 + * @rejects JavaScript exception. 1.156 + */ 1.157 + save: function DS_save() 1.158 + { 1.159 + return Task.spawn(function task_DS_save() { 1.160 + let downloads = yield this.list.getAll(); 1.161 + 1.162 + // Take a static snapshot of the current state of all the downloads. 1.163 + let storeData = { list: [] }; 1.164 + let atLeastOneDownload = false; 1.165 + for (let download of downloads) { 1.166 + try { 1.167 + if (!this.onsaveitem(download)) { 1.168 + continue; 1.169 + } 1.170 + storeData.list.push(download.toSerializable()); 1.171 + atLeastOneDownload = true; 1.172 + } catch (ex) { 1.173 + // If an item cannot be converted to a serializable form, don't 1.174 + // prevent others from being saved. 1.175 + Cu.reportError(ex); 1.176 + } 1.177 + } 1.178 + 1.179 + if (atLeastOneDownload) { 1.180 + // Create or overwrite the file if there are downloads to save. 1.181 + let bytes = gTextEncoder.encode(JSON.stringify(storeData)); 1.182 + yield OS.File.writeAtomic(this.path, bytes, 1.183 + { tmpPath: this.path + ".tmp" }); 1.184 + } else { 1.185 + // Remove the file if there are no downloads to save at all. 1.186 + try { 1.187 + yield OS.File.remove(this.path); 1.188 + } catch (ex if ex instanceof OS.File.Error && 1.189 + (ex.becauseNoSuchFile || ex.becauseAccessDenied)) { 1.190 + // On Windows, we may get an access denied error instead of a no such 1.191 + // file error if the file existed before, and was recently deleted. 1.192 + } 1.193 + } 1.194 + }.bind(this)); 1.195 + }, 1.196 +};