Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | /** |
michael@0 | 8 | * This file includes the following constructors and global objects: |
michael@0 | 9 | * |
michael@0 | 10 | * Download |
michael@0 | 11 | * Represents a single download, with associated state and actions. This object |
michael@0 | 12 | * is transient, though it can be included in a DownloadList so that it can be |
michael@0 | 13 | * managed by the user interface and persisted across sessions. |
michael@0 | 14 | * |
michael@0 | 15 | * DownloadSource |
michael@0 | 16 | * Represents the source of a download, for example a document or an URI. |
michael@0 | 17 | * |
michael@0 | 18 | * DownloadTarget |
michael@0 | 19 | * Represents the target of a download, for example a file in the global |
michael@0 | 20 | * downloads directory, or a file in the system temporary directory. |
michael@0 | 21 | * |
michael@0 | 22 | * DownloadError |
michael@0 | 23 | * Provides detailed information about a download failure. |
michael@0 | 24 | * |
michael@0 | 25 | * DownloadSaver |
michael@0 | 26 | * Template for an object that actually transfers the data for the download. |
michael@0 | 27 | * |
michael@0 | 28 | * DownloadCopySaver |
michael@0 | 29 | * Saver object that simply copies the entire source file to the target. |
michael@0 | 30 | * |
michael@0 | 31 | * DownloadLegacySaver |
michael@0 | 32 | * Saver object that integrates with the legacy nsITransfer interface. |
michael@0 | 33 | */ |
michael@0 | 34 | |
michael@0 | 35 | "use strict"; |
michael@0 | 36 | |
michael@0 | 37 | this.EXPORTED_SYMBOLS = [ |
michael@0 | 38 | "Download", |
michael@0 | 39 | "DownloadSource", |
michael@0 | 40 | "DownloadTarget", |
michael@0 | 41 | "DownloadError", |
michael@0 | 42 | "DownloadSaver", |
michael@0 | 43 | "DownloadCopySaver", |
michael@0 | 44 | "DownloadLegacySaver", |
michael@0 | 45 | ]; |
michael@0 | 46 | |
michael@0 | 47 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 48 | //// Globals |
michael@0 | 49 | |
michael@0 | 50 | const Cc = Components.classes; |
michael@0 | 51 | const Ci = Components.interfaces; |
michael@0 | 52 | const Cu = Components.utils; |
michael@0 | 53 | const Cr = Components.results; |
michael@0 | 54 | |
michael@0 | 55 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 56 | |
michael@0 | 57 | XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration", |
michael@0 | 58 | "resource://gre/modules/DownloadIntegration.jsm"); |
michael@0 | 59 | XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
michael@0 | 60 | "resource://gre/modules/FileUtils.jsm"); |
michael@0 | 61 | XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
michael@0 | 62 | "resource://gre/modules/NetUtil.jsm"); |
michael@0 | 63 | XPCOMUtils.defineLazyModuleGetter(this, "OS", |
michael@0 | 64 | "resource://gre/modules/osfile.jsm") |
michael@0 | 65 | XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
michael@0 | 66 | "resource://gre/modules/Promise.jsm"); |
michael@0 | 67 | XPCOMUtils.defineLazyModuleGetter(this, "Services", |
michael@0 | 68 | "resource://gre/modules/Services.jsm"); |
michael@0 | 69 | XPCOMUtils.defineLazyModuleGetter(this, "Task", |
michael@0 | 70 | "resource://gre/modules/Task.jsm"); |
michael@0 | 71 | |
michael@0 | 72 | XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory", |
michael@0 | 73 | "@mozilla.org/browser/download-history;1", |
michael@0 | 74 | Ci.nsIDownloadHistory); |
michael@0 | 75 | XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher", |
michael@0 | 76 | "@mozilla.org/uriloader/external-helper-app-service;1", |
michael@0 | 77 | Ci.nsPIExternalAppLauncher); |
michael@0 | 78 | XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService", |
michael@0 | 79 | "@mozilla.org/uriloader/external-helper-app-service;1", |
michael@0 | 80 | Ci.nsIExternalHelperAppService); |
michael@0 | 81 | |
michael@0 | 82 | const BackgroundFileSaverStreamListener = Components.Constructor( |
michael@0 | 83 | "@mozilla.org/network/background-file-saver;1?mode=streamlistener", |
michael@0 | 84 | "nsIBackgroundFileSaver"); |
michael@0 | 85 | |
michael@0 | 86 | /** |
michael@0 | 87 | * Returns true if the given value is a primitive string or a String object. |
michael@0 | 88 | */ |
michael@0 | 89 | function isString(aValue) { |
michael@0 | 90 | // We cannot use the "instanceof" operator reliably across module boundaries. |
michael@0 | 91 | return (typeof aValue == "string") || |
michael@0 | 92 | (typeof aValue == "object" && "charAt" in aValue); |
michael@0 | 93 | } |
michael@0 | 94 | |
michael@0 | 95 | /** |
michael@0 | 96 | * Serialize the unknown properties of aObject into aSerializable. |
michael@0 | 97 | */ |
michael@0 | 98 | function serializeUnknownProperties(aObject, aSerializable) |
michael@0 | 99 | { |
michael@0 | 100 | if (aObject._unknownProperties) { |
michael@0 | 101 | for (let property in aObject._unknownProperties) { |
michael@0 | 102 | aSerializable[property] = aObject._unknownProperties[property]; |
michael@0 | 103 | } |
michael@0 | 104 | } |
michael@0 | 105 | } |
michael@0 | 106 | |
michael@0 | 107 | /** |
michael@0 | 108 | * Check for any unknown properties in aSerializable and preserve those in the |
michael@0 | 109 | * _unknownProperties field of aObject. aFilterFn is called for each property |
michael@0 | 110 | * name of aObject and should return true only for unknown properties. |
michael@0 | 111 | */ |
michael@0 | 112 | function deserializeUnknownProperties(aObject, aSerializable, aFilterFn) |
michael@0 | 113 | { |
michael@0 | 114 | for (let property in aSerializable) { |
michael@0 | 115 | if (aFilterFn(property)) { |
michael@0 | 116 | if (!aObject._unknownProperties) { |
michael@0 | 117 | aObject._unknownProperties = { }; |
michael@0 | 118 | } |
michael@0 | 119 | |
michael@0 | 120 | aObject._unknownProperties[property] = aSerializable[property]; |
michael@0 | 121 | } |
michael@0 | 122 | } |
michael@0 | 123 | } |
michael@0 | 124 | |
michael@0 | 125 | /** |
michael@0 | 126 | * This determines the minimum time interval between updates to the number of |
michael@0 | 127 | * bytes transferred, and is a limiting factor to the sequence of readings used |
michael@0 | 128 | * in calculating the speed of the download. |
michael@0 | 129 | */ |
michael@0 | 130 | const kProgressUpdateIntervalMs = 400; |
michael@0 | 131 | |
michael@0 | 132 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 133 | //// Download |
michael@0 | 134 | |
michael@0 | 135 | /** |
michael@0 | 136 | * Represents a single download, with associated state and actions. This object |
michael@0 | 137 | * is transient, though it can be included in a DownloadList so that it can be |
michael@0 | 138 | * managed by the user interface and persisted across sessions. |
michael@0 | 139 | */ |
michael@0 | 140 | this.Download = function () |
michael@0 | 141 | { |
michael@0 | 142 | this._deferSucceeded = Promise.defer(); |
michael@0 | 143 | } |
michael@0 | 144 | |
michael@0 | 145 | this.Download.prototype = { |
michael@0 | 146 | /** |
michael@0 | 147 | * DownloadSource object associated with this download. |
michael@0 | 148 | */ |
michael@0 | 149 | source: null, |
michael@0 | 150 | |
michael@0 | 151 | /** |
michael@0 | 152 | * DownloadTarget object associated with this download. |
michael@0 | 153 | */ |
michael@0 | 154 | target: null, |
michael@0 | 155 | |
michael@0 | 156 | /** |
michael@0 | 157 | * DownloadSaver object associated with this download. |
michael@0 | 158 | */ |
michael@0 | 159 | saver: null, |
michael@0 | 160 | |
michael@0 | 161 | /** |
michael@0 | 162 | * Indicates that the download never started, has been completed successfully, |
michael@0 | 163 | * failed, or has been canceled. This property becomes false when a download |
michael@0 | 164 | * is started for the first time, or when a failed or canceled download is |
michael@0 | 165 | * restarted. |
michael@0 | 166 | */ |
michael@0 | 167 | stopped: true, |
michael@0 | 168 | |
michael@0 | 169 | /** |
michael@0 | 170 | * Indicates that the download has been completed successfully. |
michael@0 | 171 | */ |
michael@0 | 172 | succeeded: false, |
michael@0 | 173 | |
michael@0 | 174 | /** |
michael@0 | 175 | * Indicates that the download has been canceled. This property can become |
michael@0 | 176 | * true, then it can be reset to false when a canceled download is restarted. |
michael@0 | 177 | * |
michael@0 | 178 | * This property becomes true as soon as the "cancel" method is called, though |
michael@0 | 179 | * the "stopped" property might remain false until the cancellation request |
michael@0 | 180 | * has been processed. Temporary files or part files may still exist even if |
michael@0 | 181 | * they are expected to be deleted, until the "stopped" property becomes true. |
michael@0 | 182 | */ |
michael@0 | 183 | canceled: false, |
michael@0 | 184 | |
michael@0 | 185 | /** |
michael@0 | 186 | * When the download fails, this is set to a DownloadError instance indicating |
michael@0 | 187 | * the cause of the failure. If the download has been completed successfully |
michael@0 | 188 | * or has been canceled, this property is null. This property is reset to |
michael@0 | 189 | * null when a failed download is restarted. |
michael@0 | 190 | */ |
michael@0 | 191 | error: null, |
michael@0 | 192 | |
michael@0 | 193 | /** |
michael@0 | 194 | * Indicates the start time of the download. When the download starts, |
michael@0 | 195 | * this property is set to a valid Date object. The default value is null |
michael@0 | 196 | * before the download starts. |
michael@0 | 197 | */ |
michael@0 | 198 | startTime: null, |
michael@0 | 199 | |
michael@0 | 200 | /** |
michael@0 | 201 | * Indicates whether this download's "progress" property is able to report |
michael@0 | 202 | * partial progress while the download proceeds, and whether the value in |
michael@0 | 203 | * totalBytes is relevant. This depends on the saver and the download source. |
michael@0 | 204 | */ |
michael@0 | 205 | hasProgress: false, |
michael@0 | 206 | |
michael@0 | 207 | /** |
michael@0 | 208 | * Progress percent, from 0 to 100. Intermediate values are reported only if |
michael@0 | 209 | * hasProgress is true. |
michael@0 | 210 | * |
michael@0 | 211 | * @note You shouldn't rely on this property being equal to 100 to determine |
michael@0 | 212 | * whether the download is completed. You should use the individual |
michael@0 | 213 | * state properties instead. |
michael@0 | 214 | */ |
michael@0 | 215 | progress: 0, |
michael@0 | 216 | |
michael@0 | 217 | /** |
michael@0 | 218 | * When hasProgress is true, indicates the total number of bytes to be |
michael@0 | 219 | * transferred before the download finishes, that can be zero for empty files. |
michael@0 | 220 | * |
michael@0 | 221 | * When hasProgress is false, this property is always zero. |
michael@0 | 222 | */ |
michael@0 | 223 | totalBytes: 0, |
michael@0 | 224 | |
michael@0 | 225 | /** |
michael@0 | 226 | * Number of bytes currently transferred. This value starts at zero, and may |
michael@0 | 227 | * be updated regardless of the value of hasProgress. |
michael@0 | 228 | * |
michael@0 | 229 | * @note You shouldn't rely on this property being equal to totalBytes to |
michael@0 | 230 | * determine whether the download is completed. You should use the |
michael@0 | 231 | * individual state properties instead. |
michael@0 | 232 | */ |
michael@0 | 233 | currentBytes: 0, |
michael@0 | 234 | |
michael@0 | 235 | /** |
michael@0 | 236 | * Fractional number representing the speed of the download, in bytes per |
michael@0 | 237 | * second. This value is zero when the download is stopped, and may be |
michael@0 | 238 | * updated regardless of the value of hasProgress. |
michael@0 | 239 | */ |
michael@0 | 240 | speed: 0, |
michael@0 | 241 | |
michael@0 | 242 | /** |
michael@0 | 243 | * Indicates whether, at this time, there is any partially downloaded data |
michael@0 | 244 | * that can be used when restarting a failed or canceled download. |
michael@0 | 245 | * |
michael@0 | 246 | * This property is relevant while the download is in progress, and also if it |
michael@0 | 247 | * failed or has been canceled. If the download has been completed |
michael@0 | 248 | * successfully, this property is always false. |
michael@0 | 249 | * |
michael@0 | 250 | * Whether partial data can actually be retained depends on the saver and the |
michael@0 | 251 | * download source, and may not be known before the download is started. |
michael@0 | 252 | */ |
michael@0 | 253 | hasPartialData: false, |
michael@0 | 254 | |
michael@0 | 255 | /** |
michael@0 | 256 | * This can be set to a function that is called after other properties change. |
michael@0 | 257 | */ |
michael@0 | 258 | onchange: null, |
michael@0 | 259 | |
michael@0 | 260 | /** |
michael@0 | 261 | * This tells if the user has chosen to open/run the downloaded file after |
michael@0 | 262 | * download has completed. |
michael@0 | 263 | */ |
michael@0 | 264 | launchWhenSucceeded: false, |
michael@0 | 265 | |
michael@0 | 266 | /** |
michael@0 | 267 | * This represents the MIME type of the download. |
michael@0 | 268 | */ |
michael@0 | 269 | contentType: null, |
michael@0 | 270 | |
michael@0 | 271 | /** |
michael@0 | 272 | * This indicates the path of the application to be used to launch the file, |
michael@0 | 273 | * or null if the file should be launched with the default application. |
michael@0 | 274 | */ |
michael@0 | 275 | launcherPath: null, |
michael@0 | 276 | |
michael@0 | 277 | /** |
michael@0 | 278 | * Raises the onchange notification. |
michael@0 | 279 | */ |
michael@0 | 280 | _notifyChange: function D_notifyChange() { |
michael@0 | 281 | try { |
michael@0 | 282 | if (this.onchange) { |
michael@0 | 283 | this.onchange(); |
michael@0 | 284 | } |
michael@0 | 285 | } catch (ex) { |
michael@0 | 286 | Cu.reportError(ex); |
michael@0 | 287 | } |
michael@0 | 288 | }, |
michael@0 | 289 | |
michael@0 | 290 | /** |
michael@0 | 291 | * The download may be stopped and restarted multiple times before it |
michael@0 | 292 | * completes successfully. This may happen if any of the download attempts is |
michael@0 | 293 | * canceled or fails. |
michael@0 | 294 | * |
michael@0 | 295 | * This property contains a promise that is linked to the current attempt, or |
michael@0 | 296 | * null if the download is either stopped or in the process of being canceled. |
michael@0 | 297 | * If the download restarts, this property is replaced with a new promise. |
michael@0 | 298 | * |
michael@0 | 299 | * The promise is resolved if the attempt it represents finishes successfully, |
michael@0 | 300 | * and rejected if the attempt fails. |
michael@0 | 301 | */ |
michael@0 | 302 | _currentAttempt: null, |
michael@0 | 303 | |
michael@0 | 304 | /** |
michael@0 | 305 | * Starts the download for the first time, or restarts a download that failed |
michael@0 | 306 | * or has been canceled. |
michael@0 | 307 | * |
michael@0 | 308 | * Calling this method when the download has been completed successfully has |
michael@0 | 309 | * no effect, and the method returns a resolved promise. If the download is |
michael@0 | 310 | * in progress, the method returns the same promise as the previous call. |
michael@0 | 311 | * |
michael@0 | 312 | * If the "cancel" method was called but the cancellation process has not |
michael@0 | 313 | * finished yet, this method waits for the cancellation to finish, then |
michael@0 | 314 | * restarts the download immediately. |
michael@0 | 315 | * |
michael@0 | 316 | * @note If you need to start a new download from the same source, rather than |
michael@0 | 317 | * restarting a failed or canceled one, you should create a separate |
michael@0 | 318 | * Download object with the same source as the current one. |
michael@0 | 319 | * |
michael@0 | 320 | * @return {Promise} |
michael@0 | 321 | * @resolves When the download has finished successfully. |
michael@0 | 322 | * @rejects JavaScript exception if the download failed. |
michael@0 | 323 | */ |
michael@0 | 324 | start: function D_start() |
michael@0 | 325 | { |
michael@0 | 326 | // If the download succeeded, it's the final state, we have nothing to do. |
michael@0 | 327 | if (this.succeeded) { |
michael@0 | 328 | return Promise.resolve(); |
michael@0 | 329 | } |
michael@0 | 330 | |
michael@0 | 331 | // If the download already started and hasn't failed or hasn't been |
michael@0 | 332 | // canceled, return the same promise as the previous call, allowing the |
michael@0 | 333 | // caller to wait for the current attempt to finish. |
michael@0 | 334 | if (this._currentAttempt) { |
michael@0 | 335 | return this._currentAttempt; |
michael@0 | 336 | } |
michael@0 | 337 | |
michael@0 | 338 | // While shutting down or disposing of this object, we prevent the download |
michael@0 | 339 | // from returning to be in progress. |
michael@0 | 340 | if (this._finalized) { |
michael@0 | 341 | return Promise.reject(new DownloadError({ |
michael@0 | 342 | message: "Cannot start after finalization."})); |
michael@0 | 343 | } |
michael@0 | 344 | |
michael@0 | 345 | // Initialize all the status properties for a new or restarted download. |
michael@0 | 346 | this.stopped = false; |
michael@0 | 347 | this.canceled = false; |
michael@0 | 348 | this.error = null; |
michael@0 | 349 | this.hasProgress = false; |
michael@0 | 350 | this.progress = 0; |
michael@0 | 351 | this.totalBytes = 0; |
michael@0 | 352 | this.currentBytes = 0; |
michael@0 | 353 | this.startTime = new Date(); |
michael@0 | 354 | |
michael@0 | 355 | // Create a new deferred object and an associated promise before starting |
michael@0 | 356 | // the actual download. We store it on the download as the current attempt. |
michael@0 | 357 | let deferAttempt = Promise.defer(); |
michael@0 | 358 | let currentAttempt = deferAttempt.promise; |
michael@0 | 359 | this._currentAttempt = currentAttempt; |
michael@0 | 360 | |
michael@0 | 361 | // Restart the progress and speed calculations from scratch. |
michael@0 | 362 | this._lastProgressTimeMs = 0; |
michael@0 | 363 | |
michael@0 | 364 | // This function propagates progress from the DownloadSaver object, unless |
michael@0 | 365 | // it comes in late from a download attempt that was replaced by a new one. |
michael@0 | 366 | // If the cancellation process for the download has started, then the update |
michael@0 | 367 | // is ignored. |
michael@0 | 368 | function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData) |
michael@0 | 369 | { |
michael@0 | 370 | if (this._currentAttempt == currentAttempt) { |
michael@0 | 371 | this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData); |
michael@0 | 372 | } |
michael@0 | 373 | } |
michael@0 | 374 | |
michael@0 | 375 | // This function propagates download properties from the DownloadSaver |
michael@0 | 376 | // object, unless it comes in late from a download attempt that was |
michael@0 | 377 | // replaced by a new one. If the cancellation process for the download has |
michael@0 | 378 | // started, then the update is ignored. |
michael@0 | 379 | function DS_setProperties(aOptions) |
michael@0 | 380 | { |
michael@0 | 381 | if (this._currentAttempt != currentAttempt) { |
michael@0 | 382 | return; |
michael@0 | 383 | } |
michael@0 | 384 | |
michael@0 | 385 | let changeMade = false; |
michael@0 | 386 | |
michael@0 | 387 | if ("contentType" in aOptions && |
michael@0 | 388 | this.contentType != aOptions.contentType) { |
michael@0 | 389 | this.contentType = aOptions.contentType; |
michael@0 | 390 | changeMade = true; |
michael@0 | 391 | } |
michael@0 | 392 | |
michael@0 | 393 | if (changeMade) { |
michael@0 | 394 | this._notifyChange(); |
michael@0 | 395 | } |
michael@0 | 396 | } |
michael@0 | 397 | |
michael@0 | 398 | // Now that we stored the promise in the download object, we can start the |
michael@0 | 399 | // task that will actually execute the download. |
michael@0 | 400 | deferAttempt.resolve(Task.spawn(function task_D_start() { |
michael@0 | 401 | // Wait upon any pending operation before restarting. |
michael@0 | 402 | if (this._promiseCanceled) { |
michael@0 | 403 | yield this._promiseCanceled; |
michael@0 | 404 | } |
michael@0 | 405 | if (this._promiseRemovePartialData) { |
michael@0 | 406 | try { |
michael@0 | 407 | yield this._promiseRemovePartialData; |
michael@0 | 408 | } catch (ex) { |
michael@0 | 409 | // Ignore any errors, which are already reported by the original |
michael@0 | 410 | // caller of the removePartialData method. |
michael@0 | 411 | } |
michael@0 | 412 | } |
michael@0 | 413 | |
michael@0 | 414 | // In case the download was restarted while cancellation was in progress, |
michael@0 | 415 | // but the previous attempt actually succeeded before cancellation could |
michael@0 | 416 | // be processed, it is possible that the download has already finished. |
michael@0 | 417 | if (this.succeeded) { |
michael@0 | 418 | return; |
michael@0 | 419 | } |
michael@0 | 420 | |
michael@0 | 421 | try { |
michael@0 | 422 | // Disallow download if parental controls service restricts it. |
michael@0 | 423 | if (yield DownloadIntegration.shouldBlockForParentalControls(this)) { |
michael@0 | 424 | throw new DownloadError({ becauseBlockedByParentalControls: true }); |
michael@0 | 425 | } |
michael@0 | 426 | |
michael@0 | 427 | // We should check if we have been canceled in the meantime, after all |
michael@0 | 428 | // the previous asynchronous operations have been executed and just |
michael@0 | 429 | // before we call the "execute" method of the saver. |
michael@0 | 430 | if (this._promiseCanceled) { |
michael@0 | 431 | // The exception will become a cancellation in the "catch" block. |
michael@0 | 432 | throw undefined; |
michael@0 | 433 | } |
michael@0 | 434 | |
michael@0 | 435 | // Execute the actual download through the saver object. |
michael@0 | 436 | this._saverExecuting = true; |
michael@0 | 437 | yield this.saver.execute(DS_setProgressBytes.bind(this), |
michael@0 | 438 | DS_setProperties.bind(this)); |
michael@0 | 439 | |
michael@0 | 440 | // Check for application reputation, which requires the entire file to |
michael@0 | 441 | // be downloaded. After that, check for the last time if the download |
michael@0 | 442 | // has been canceled. Both cases require the target file to be deleted, |
michael@0 | 443 | // thus we process both in the same block of code. |
michael@0 | 444 | if ((yield DownloadIntegration.shouldBlockForReputationCheck(this)) || |
michael@0 | 445 | this._promiseCanceled) { |
michael@0 | 446 | try { |
michael@0 | 447 | yield OS.File.remove(this.target.path); |
michael@0 | 448 | } catch (ex) { |
michael@0 | 449 | Cu.reportError(ex); |
michael@0 | 450 | } |
michael@0 | 451 | // If this is actually a cancellation, this exception will be changed |
michael@0 | 452 | // in the catch block below. |
michael@0 | 453 | throw new DownloadError({ becauseBlockedByReputationCheck: true }); |
michael@0 | 454 | } |
michael@0 | 455 | |
michael@0 | 456 | // Update the status properties for a successful download. |
michael@0 | 457 | this.progress = 100; |
michael@0 | 458 | this.succeeded = true; |
michael@0 | 459 | this.hasPartialData = false; |
michael@0 | 460 | } catch (ex) { |
michael@0 | 461 | // Fail with a generic status code on cancellation, so that the caller |
michael@0 | 462 | // is forced to actually check the status properties to see if the |
michael@0 | 463 | // download was canceled or failed because of other reasons. |
michael@0 | 464 | if (this._promiseCanceled) { |
michael@0 | 465 | throw new DownloadError({ message: "Download canceled." }); |
michael@0 | 466 | } |
michael@0 | 467 | |
michael@0 | 468 | // An HTTP 450 error code is used by Windows to indicate that a uri is |
michael@0 | 469 | // blocked by parental controls. This will prevent the download from |
michael@0 | 470 | // occuring, so an error needs to be raised. This is not performed |
michael@0 | 471 | // during the parental controls check above as it requires the request |
michael@0 | 472 | // to start. |
michael@0 | 473 | if (this._blockedByParentalControls) { |
michael@0 | 474 | ex = new DownloadError({ becauseBlockedByParentalControls: true }); |
michael@0 | 475 | } |
michael@0 | 476 | |
michael@0 | 477 | // Update the download error, unless a new attempt already started. The |
michael@0 | 478 | // change in the status property is notified in the finally block. |
michael@0 | 479 | if (this._currentAttempt == currentAttempt || !this._currentAttempt) { |
michael@0 | 480 | this.error = ex; |
michael@0 | 481 | } |
michael@0 | 482 | throw ex; |
michael@0 | 483 | } finally { |
michael@0 | 484 | // Any cancellation request has now been processed. |
michael@0 | 485 | this._saverExecuting = false; |
michael@0 | 486 | this._promiseCanceled = null; |
michael@0 | 487 | |
michael@0 | 488 | // Update the status properties, unless a new attempt already started. |
michael@0 | 489 | if (this._currentAttempt == currentAttempt || !this._currentAttempt) { |
michael@0 | 490 | this._currentAttempt = null; |
michael@0 | 491 | this.stopped = true; |
michael@0 | 492 | this.speed = 0; |
michael@0 | 493 | this._notifyChange(); |
michael@0 | 494 | if (this.succeeded) { |
michael@0 | 495 | yield DownloadIntegration.downloadDone(this); |
michael@0 | 496 | |
michael@0 | 497 | this._deferSucceeded.resolve(); |
michael@0 | 498 | |
michael@0 | 499 | if (this.launchWhenSucceeded) { |
michael@0 | 500 | this.launch().then(null, Cu.reportError); |
michael@0 | 501 | |
michael@0 | 502 | // Always schedule files to be deleted at the end of the private browsing |
michael@0 | 503 | // mode, regardless of the value of the pref. |
michael@0 | 504 | if (this.source.isPrivate) { |
michael@0 | 505 | gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible( |
michael@0 | 506 | new FileUtils.File(this.target.path)); |
michael@0 | 507 | } else if (Services.prefs.getBoolPref( |
michael@0 | 508 | "browser.helperApps.deleteTempFileOnExit")) { |
michael@0 | 509 | gExternalAppLauncher.deleteTemporaryFileOnExit( |
michael@0 | 510 | new FileUtils.File(this.target.path)); |
michael@0 | 511 | } |
michael@0 | 512 | } |
michael@0 | 513 | } |
michael@0 | 514 | } |
michael@0 | 515 | } |
michael@0 | 516 | }.bind(this))); |
michael@0 | 517 | |
michael@0 | 518 | // Notify the new download state before returning. |
michael@0 | 519 | this._notifyChange(); |
michael@0 | 520 | return currentAttempt; |
michael@0 | 521 | }, |
michael@0 | 522 | |
michael@0 | 523 | /* |
michael@0 | 524 | * Launches the file after download has completed. This can open |
michael@0 | 525 | * the file with the default application for the target MIME type |
michael@0 | 526 | * or file extension, or with a custom application if launcherPath |
michael@0 | 527 | * is set. |
michael@0 | 528 | * |
michael@0 | 529 | * @return {Promise} |
michael@0 | 530 | * @resolves When the instruction to launch the file has been |
michael@0 | 531 | * successfully given to the operating system. Note that |
michael@0 | 532 | * the OS might still take a while until the file is actually |
michael@0 | 533 | * launched. |
michael@0 | 534 | * @rejects JavaScript exception if there was an error trying to launch |
michael@0 | 535 | * the file. |
michael@0 | 536 | */ |
michael@0 | 537 | launch: function() { |
michael@0 | 538 | if (!this.succeeded) { |
michael@0 | 539 | return Promise.reject( |
michael@0 | 540 | new Error("launch can only be called if the download succeeded") |
michael@0 | 541 | ); |
michael@0 | 542 | } |
michael@0 | 543 | |
michael@0 | 544 | return DownloadIntegration.launchDownload(this); |
michael@0 | 545 | }, |
michael@0 | 546 | |
michael@0 | 547 | /* |
michael@0 | 548 | * Shows the folder containing the target file, or where the target file |
michael@0 | 549 | * will be saved. This may be called at any time, even if the download |
michael@0 | 550 | * failed or is currently in progress. |
michael@0 | 551 | * |
michael@0 | 552 | * @return {Promise} |
michael@0 | 553 | * @resolves When the instruction to open the containing folder has been |
michael@0 | 554 | * successfully given to the operating system. Note that |
michael@0 | 555 | * the OS might still take a while until the folder is actually |
michael@0 | 556 | * opened. |
michael@0 | 557 | * @rejects JavaScript exception if there was an error trying to open |
michael@0 | 558 | * the containing folder. |
michael@0 | 559 | */ |
michael@0 | 560 | showContainingDirectory: function D_showContainingDirectory() { |
michael@0 | 561 | return DownloadIntegration.showContainingDirectory(this.target.path); |
michael@0 | 562 | }, |
michael@0 | 563 | |
michael@0 | 564 | /** |
michael@0 | 565 | * When a request to cancel the download is received, contains a promise that |
michael@0 | 566 | * will be resolved when the cancellation request is processed. When the |
michael@0 | 567 | * request is processed, this property becomes null again. |
michael@0 | 568 | */ |
michael@0 | 569 | _promiseCanceled: null, |
michael@0 | 570 | |
michael@0 | 571 | /** |
michael@0 | 572 | * True between the call to the "execute" method of the saver and the |
michael@0 | 573 | * completion of the current download attempt. |
michael@0 | 574 | */ |
michael@0 | 575 | _saverExecuting: false, |
michael@0 | 576 | |
michael@0 | 577 | /** |
michael@0 | 578 | * Cancels the download. |
michael@0 | 579 | * |
michael@0 | 580 | * The cancellation request is asynchronous. Until the cancellation process |
michael@0 | 581 | * finishes, temporary files or part files may still exist even if they are |
michael@0 | 582 | * expected to be deleted. |
michael@0 | 583 | * |
michael@0 | 584 | * In case the download completes successfully before the cancellation request |
michael@0 | 585 | * could be processed, this method has no effect, and it returns a resolved |
michael@0 | 586 | * promise. You should check the properties of the download at the time the |
michael@0 | 587 | * returned promise is resolved to determine if the download was cancelled. |
michael@0 | 588 | * |
michael@0 | 589 | * Calling this method when the download has been completed successfully, |
michael@0 | 590 | * failed, or has been canceled has no effect, and the method returns a |
michael@0 | 591 | * resolved promise. This behavior is designed for the case where the call |
michael@0 | 592 | * to "cancel" happens asynchronously, and is consistent with the case where |
michael@0 | 593 | * the cancellation request could not be processed in time. |
michael@0 | 594 | * |
michael@0 | 595 | * @return {Promise} |
michael@0 | 596 | * @resolves When the cancellation process has finished. |
michael@0 | 597 | * @rejects Never. |
michael@0 | 598 | */ |
michael@0 | 599 | cancel: function D_cancel() |
michael@0 | 600 | { |
michael@0 | 601 | // If the download is currently stopped, we have nothing to do. |
michael@0 | 602 | if (this.stopped) { |
michael@0 | 603 | return Promise.resolve(); |
michael@0 | 604 | } |
michael@0 | 605 | |
michael@0 | 606 | if (!this._promiseCanceled) { |
michael@0 | 607 | // Start a new cancellation request. |
michael@0 | 608 | let deferCanceled = Promise.defer(); |
michael@0 | 609 | this._currentAttempt.then(function () deferCanceled.resolve(), |
michael@0 | 610 | function () deferCanceled.resolve()); |
michael@0 | 611 | this._promiseCanceled = deferCanceled.promise; |
michael@0 | 612 | |
michael@0 | 613 | // The download can already be restarted. |
michael@0 | 614 | this._currentAttempt = null; |
michael@0 | 615 | |
michael@0 | 616 | // Notify that the cancellation request was received. |
michael@0 | 617 | this.canceled = true; |
michael@0 | 618 | this._notifyChange(); |
michael@0 | 619 | |
michael@0 | 620 | // Execute the actual cancellation through the saver object, in case it |
michael@0 | 621 | // has already started. Otherwise, the cancellation will be handled just |
michael@0 | 622 | // before the saver is started. |
michael@0 | 623 | if (this._saverExecuting) { |
michael@0 | 624 | this.saver.cancel(); |
michael@0 | 625 | } |
michael@0 | 626 | } |
michael@0 | 627 | |
michael@0 | 628 | return this._promiseCanceled; |
michael@0 | 629 | }, |
michael@0 | 630 | |
michael@0 | 631 | /** |
michael@0 | 632 | * Indicates whether any partially downloaded data should be retained, to use |
michael@0 | 633 | * when restarting a failed or canceled download. The default is false. |
michael@0 | 634 | * |
michael@0 | 635 | * Whether partial data can actually be retained depends on the saver and the |
michael@0 | 636 | * download source, and may not be known before the download is started. |
michael@0 | 637 | * |
michael@0 | 638 | * To have any effect, this property must be set before starting the download. |
michael@0 | 639 | * Resetting this property to false after the download has already started |
michael@0 | 640 | * will not remove any partial data. |
michael@0 | 641 | * |
michael@0 | 642 | * If this property is set to true, care should be taken that partial data is |
michael@0 | 643 | * removed before the reference to the download is discarded. This can be |
michael@0 | 644 | * done using the removePartialData or the "finalize" methods. |
michael@0 | 645 | */ |
michael@0 | 646 | tryToKeepPartialData: false, |
michael@0 | 647 | |
michael@0 | 648 | /** |
michael@0 | 649 | * When a request to remove partially downloaded data is received, contains a |
michael@0 | 650 | * promise that will be resolved when the removal request is processed. When |
michael@0 | 651 | * the request is processed, this property becomes null again. |
michael@0 | 652 | */ |
michael@0 | 653 | _promiseRemovePartialData: null, |
michael@0 | 654 | |
michael@0 | 655 | /** |
michael@0 | 656 | * Removes any partial data kept as part of a canceled or failed download. |
michael@0 | 657 | * |
michael@0 | 658 | * If the download is not canceled or failed, this method has no effect, and |
michael@0 | 659 | * it returns a resolved promise. If the "cancel" method was called but the |
michael@0 | 660 | * cancellation process has not finished yet, this method waits for the |
michael@0 | 661 | * cancellation to finish, then removes the partial data. |
michael@0 | 662 | * |
michael@0 | 663 | * After this method has been called, if the tryToKeepPartialData property is |
michael@0 | 664 | * still true when the download is restarted, partial data will be retained |
michael@0 | 665 | * during the new download attempt. |
michael@0 | 666 | * |
michael@0 | 667 | * @return {Promise} |
michael@0 | 668 | * @resolves When the partial data has been successfully removed. |
michael@0 | 669 | * @rejects JavaScript exception if the operation could not be completed. |
michael@0 | 670 | */ |
michael@0 | 671 | removePartialData: function () |
michael@0 | 672 | { |
michael@0 | 673 | if (!this.canceled && !this.error) { |
michael@0 | 674 | return Promise.resolve(); |
michael@0 | 675 | } |
michael@0 | 676 | |
michael@0 | 677 | let promiseRemovePartialData = this._promiseRemovePartialData; |
michael@0 | 678 | |
michael@0 | 679 | if (!promiseRemovePartialData) { |
michael@0 | 680 | let deferRemovePartialData = Promise.defer(); |
michael@0 | 681 | promiseRemovePartialData = deferRemovePartialData.promise; |
michael@0 | 682 | this._promiseRemovePartialData = promiseRemovePartialData; |
michael@0 | 683 | |
michael@0 | 684 | deferRemovePartialData.resolve( |
michael@0 | 685 | Task.spawn(function task_D_removePartialData() { |
michael@0 | 686 | try { |
michael@0 | 687 | // Wait upon any pending cancellation request. |
michael@0 | 688 | if (this._promiseCanceled) { |
michael@0 | 689 | yield this._promiseCanceled; |
michael@0 | 690 | } |
michael@0 | 691 | // Ask the saver object to remove any partial data. |
michael@0 | 692 | yield this.saver.removePartialData(); |
michael@0 | 693 | // For completeness, clear the number of bytes transferred. |
michael@0 | 694 | if (this.currentBytes != 0 || this.hasPartialData) { |
michael@0 | 695 | this.currentBytes = 0; |
michael@0 | 696 | this.hasPartialData = false; |
michael@0 | 697 | this._notifyChange(); |
michael@0 | 698 | } |
michael@0 | 699 | } finally { |
michael@0 | 700 | this._promiseRemovePartialData = null; |
michael@0 | 701 | } |
michael@0 | 702 | }.bind(this))); |
michael@0 | 703 | } |
michael@0 | 704 | |
michael@0 | 705 | return promiseRemovePartialData; |
michael@0 | 706 | }, |
michael@0 | 707 | |
michael@0 | 708 | /** |
michael@0 | 709 | * This deferred object contains a promise that is resolved as soon as this |
michael@0 | 710 | * download finishes successfully, and is never rejected. This property is |
michael@0 | 711 | * initialized when the download is created, and never changes. |
michael@0 | 712 | */ |
michael@0 | 713 | _deferSucceeded: null, |
michael@0 | 714 | |
michael@0 | 715 | /** |
michael@0 | 716 | * Returns a promise that is resolved as soon as this download finishes |
michael@0 | 717 | * successfully, even if the download was stopped and restarted meanwhile. |
michael@0 | 718 | * |
michael@0 | 719 | * You can use this property for scheduling download completion actions in the |
michael@0 | 720 | * current session, for downloads that are controlled interactively. If the |
michael@0 | 721 | * download is not controlled interactively, you should use the promise |
michael@0 | 722 | * returned by the "start" method instead, to check for success or failure. |
michael@0 | 723 | * |
michael@0 | 724 | * @return {Promise} |
michael@0 | 725 | * @resolves When the download has finished successfully. |
michael@0 | 726 | * @rejects Never. |
michael@0 | 727 | */ |
michael@0 | 728 | whenSucceeded: function D_whenSucceeded() |
michael@0 | 729 | { |
michael@0 | 730 | return this._deferSucceeded.promise; |
michael@0 | 731 | }, |
michael@0 | 732 | |
michael@0 | 733 | /** |
michael@0 | 734 | * Updates the state of a finished, failed, or canceled download based on the |
michael@0 | 735 | * current state in the file system. If the download is in progress or it has |
michael@0 | 736 | * been finalized, this method has no effect, and it returns a resolved |
michael@0 | 737 | * promise. |
michael@0 | 738 | * |
michael@0 | 739 | * This allows the properties of the download to be updated in case the user |
michael@0 | 740 | * moved or deleted the target file or its associated ".part" file. |
michael@0 | 741 | * |
michael@0 | 742 | * @return {Promise} |
michael@0 | 743 | * @resolves When the operation has completed. |
michael@0 | 744 | * @rejects Never. |
michael@0 | 745 | */ |
michael@0 | 746 | refresh: function () |
michael@0 | 747 | { |
michael@0 | 748 | return Task.spawn(function () { |
michael@0 | 749 | if (!this.stopped || this._finalized) { |
michael@0 | 750 | return; |
michael@0 | 751 | } |
michael@0 | 752 | |
michael@0 | 753 | // Update the current progress from disk if we retained partial data. |
michael@0 | 754 | if (this.hasPartialData && this.target.partFilePath) { |
michael@0 | 755 | let stat = yield OS.File.stat(this.target.partFilePath); |
michael@0 | 756 | |
michael@0 | 757 | // Ignore the result if the state has changed meanwhile. |
michael@0 | 758 | if (!this.stopped || this._finalized) { |
michael@0 | 759 | return; |
michael@0 | 760 | } |
michael@0 | 761 | |
michael@0 | 762 | // Update the bytes transferred and the related progress properties. |
michael@0 | 763 | this.currentBytes = stat.size; |
michael@0 | 764 | if (this.totalBytes > 0) { |
michael@0 | 765 | this.hasProgress = true; |
michael@0 | 766 | this.progress = Math.floor(this.currentBytes / |
michael@0 | 767 | this.totalBytes * 100); |
michael@0 | 768 | } |
michael@0 | 769 | this._notifyChange(); |
michael@0 | 770 | } |
michael@0 | 771 | }.bind(this)).then(null, Cu.reportError); |
michael@0 | 772 | }, |
michael@0 | 773 | |
michael@0 | 774 | /** |
michael@0 | 775 | * True if the "finalize" method has been called. This prevents the download |
michael@0 | 776 | * from starting again after having been stopped. |
michael@0 | 777 | */ |
michael@0 | 778 | _finalized: false, |
michael@0 | 779 | |
michael@0 | 780 | /** |
michael@0 | 781 | * Ensures that the download is stopped, and optionally removes any partial |
michael@0 | 782 | * data kept as part of a canceled or failed download. After this method has |
michael@0 | 783 | * been called, the download cannot be started again. |
michael@0 | 784 | * |
michael@0 | 785 | * This method should be used in place of "cancel" and removePartialData while |
michael@0 | 786 | * shutting down or disposing of the download object, to prevent other callers |
michael@0 | 787 | * from interfering with the operation. This is required because cancellation |
michael@0 | 788 | * and other operations are asynchronous. |
michael@0 | 789 | * |
michael@0 | 790 | * @param aRemovePartialData |
michael@0 | 791 | * Whether any partially downloaded data should be removed after the |
michael@0 | 792 | * download has been stopped. |
michael@0 | 793 | * |
michael@0 | 794 | * @return {Promise} |
michael@0 | 795 | * @resolves When the operation has finished successfully. |
michael@0 | 796 | * @rejects JavaScript exception if an error occurred while removing the |
michael@0 | 797 | * partially downloaded data. |
michael@0 | 798 | */ |
michael@0 | 799 | finalize: function (aRemovePartialData) |
michael@0 | 800 | { |
michael@0 | 801 | // Prevents the download from starting again after having been stopped. |
michael@0 | 802 | this._finalized = true; |
michael@0 | 803 | |
michael@0 | 804 | if (aRemovePartialData) { |
michael@0 | 805 | // Cancel the download, in case it is currently in progress, then remove |
michael@0 | 806 | // any partially downloaded data. The removal operation waits for |
michael@0 | 807 | // cancellation to be completed before resolving the promise it returns. |
michael@0 | 808 | this.cancel(); |
michael@0 | 809 | return this.removePartialData(); |
michael@0 | 810 | } else { |
michael@0 | 811 | // Just cancel the download, in case it is currently in progress. |
michael@0 | 812 | return this.cancel(); |
michael@0 | 813 | } |
michael@0 | 814 | }, |
michael@0 | 815 | |
michael@0 | 816 | /** |
michael@0 | 817 | * Indicates the time of the last progress notification, expressed as the |
michael@0 | 818 | * number of milliseconds since January 1, 1970, 00:00:00 UTC. This is zero |
michael@0 | 819 | * until some bytes have actually been transferred. |
michael@0 | 820 | */ |
michael@0 | 821 | _lastProgressTimeMs: 0, |
michael@0 | 822 | |
michael@0 | 823 | /** |
michael@0 | 824 | * Updates progress notifications based on the number of bytes transferred. |
michael@0 | 825 | * |
michael@0 | 826 | * The number of bytes transferred is not updated unless enough time passed |
michael@0 | 827 | * since this function was last called. This limits the computation load, in |
michael@0 | 828 | * particular when the listeners update the user interface in response. |
michael@0 | 829 | * |
michael@0 | 830 | * @param aCurrentBytes |
michael@0 | 831 | * Number of bytes transferred until now. |
michael@0 | 832 | * @param aTotalBytes |
michael@0 | 833 | * Total number of bytes to be transferred, or -1 if unknown. |
michael@0 | 834 | * @param aHasPartialData |
michael@0 | 835 | * Indicates whether the partially downloaded data can be used when |
michael@0 | 836 | * restarting the download if it fails or is canceled. |
michael@0 | 837 | */ |
michael@0 | 838 | _setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) { |
michael@0 | 839 | let changeMade = (this.hasPartialData != aHasPartialData); |
michael@0 | 840 | this.hasPartialData = aHasPartialData; |
michael@0 | 841 | |
michael@0 | 842 | // Unless aTotalBytes is -1, we can report partial download progress. In |
michael@0 | 843 | // this case, notify when the related properties changed since last time. |
michael@0 | 844 | if (aTotalBytes != -1 && (!this.hasProgress || |
michael@0 | 845 | this.totalBytes != aTotalBytes)) { |
michael@0 | 846 | this.hasProgress = true; |
michael@0 | 847 | this.totalBytes = aTotalBytes; |
michael@0 | 848 | changeMade = true; |
michael@0 | 849 | } |
michael@0 | 850 | |
michael@0 | 851 | // Updating the progress and computing the speed require that enough time |
michael@0 | 852 | // passed since the last update, or that we haven't started throttling yet. |
michael@0 | 853 | let currentTimeMs = Date.now(); |
michael@0 | 854 | let intervalMs = currentTimeMs - this._lastProgressTimeMs; |
michael@0 | 855 | if (intervalMs >= kProgressUpdateIntervalMs) { |
michael@0 | 856 | // Don't compute the speed unless we started throttling notifications. |
michael@0 | 857 | if (this._lastProgressTimeMs != 0) { |
michael@0 | 858 | // Calculate the speed in bytes per second. |
michael@0 | 859 | let rawSpeed = (aCurrentBytes - this.currentBytes) / intervalMs * 1000; |
michael@0 | 860 | if (this.speed == 0) { |
michael@0 | 861 | // When the previous speed is exactly zero instead of a fractional |
michael@0 | 862 | // number, this can be considered the first element of the series. |
michael@0 | 863 | this.speed = rawSpeed; |
michael@0 | 864 | } else { |
michael@0 | 865 | // Apply exponential smoothing, with a smoothing factor of 0.1. |
michael@0 | 866 | this.speed = rawSpeed * 0.1 + this.speed * 0.9; |
michael@0 | 867 | } |
michael@0 | 868 | } |
michael@0 | 869 | |
michael@0 | 870 | // Start throttling notifications only when we have actually received some |
michael@0 | 871 | // bytes for the first time. The timing of the first part of the download |
michael@0 | 872 | // is not reliable, due to possible latency in the initial notifications. |
michael@0 | 873 | // This also allows automated tests to receive and verify the number of |
michael@0 | 874 | // bytes initially transferred. |
michael@0 | 875 | if (aCurrentBytes > 0) { |
michael@0 | 876 | this._lastProgressTimeMs = currentTimeMs; |
michael@0 | 877 | |
michael@0 | 878 | // Update the progress now that we don't need its previous value. |
michael@0 | 879 | this.currentBytes = aCurrentBytes; |
michael@0 | 880 | if (this.totalBytes > 0) { |
michael@0 | 881 | this.progress = Math.floor(this.currentBytes / this.totalBytes * 100); |
michael@0 | 882 | } |
michael@0 | 883 | changeMade = true; |
michael@0 | 884 | } |
michael@0 | 885 | } |
michael@0 | 886 | |
michael@0 | 887 | if (changeMade) { |
michael@0 | 888 | this._notifyChange(); |
michael@0 | 889 | } |
michael@0 | 890 | }, |
michael@0 | 891 | |
michael@0 | 892 | /** |
michael@0 | 893 | * Returns a static representation of the current object state. |
michael@0 | 894 | * |
michael@0 | 895 | * @return A JavaScript object that can be serialized to JSON. |
michael@0 | 896 | */ |
michael@0 | 897 | toSerializable: function () |
michael@0 | 898 | { |
michael@0 | 899 | let serializable = { |
michael@0 | 900 | source: this.source.toSerializable(), |
michael@0 | 901 | target: this.target.toSerializable(), |
michael@0 | 902 | }; |
michael@0 | 903 | |
michael@0 | 904 | // Simplify the representation for the most common saver type. If the saver |
michael@0 | 905 | // is an object instead of a simple string, we can't simplify it because we |
michael@0 | 906 | // need to persist all its properties, not only "type". This may happen for |
michael@0 | 907 | // savers of type "copy" as well as other types. |
michael@0 | 908 | let saver = this.saver.toSerializable(); |
michael@0 | 909 | if (saver !== "copy") { |
michael@0 | 910 | serializable.saver = saver; |
michael@0 | 911 | } |
michael@0 | 912 | |
michael@0 | 913 | if (this.error && ("message" in this.error)) { |
michael@0 | 914 | serializable.error = { message: this.error.message }; |
michael@0 | 915 | } |
michael@0 | 916 | |
michael@0 | 917 | if (this.startTime) { |
michael@0 | 918 | serializable.startTime = this.startTime.toJSON(); |
michael@0 | 919 | } |
michael@0 | 920 | |
michael@0 | 921 | // These are serialized unless they are false, null, or empty strings. |
michael@0 | 922 | for (let property of kSerializableDownloadProperties) { |
michael@0 | 923 | if (property != "error" && property != "startTime" && this[property]) { |
michael@0 | 924 | serializable[property] = this[property]; |
michael@0 | 925 | } |
michael@0 | 926 | } |
michael@0 | 927 | |
michael@0 | 928 | serializeUnknownProperties(this, serializable); |
michael@0 | 929 | |
michael@0 | 930 | return serializable; |
michael@0 | 931 | }, |
michael@0 | 932 | |
michael@0 | 933 | /** |
michael@0 | 934 | * Returns a value that changes only when one of the properties of a Download |
michael@0 | 935 | * object that should be saved into a file also change. This excludes |
michael@0 | 936 | * properties whose value doesn't usually change during the download lifetime. |
michael@0 | 937 | * |
michael@0 | 938 | * This function is used to determine whether the download should be |
michael@0 | 939 | * serialized after a property change notification has been received. |
michael@0 | 940 | * |
michael@0 | 941 | * @return String representing the relevant download state. |
michael@0 | 942 | */ |
michael@0 | 943 | getSerializationHash: function () |
michael@0 | 944 | { |
michael@0 | 945 | // The "succeeded", "canceled", "error", and startTime properties are not |
michael@0 | 946 | // taken into account because they all change before the "stopped" property |
michael@0 | 947 | // changes, and are not altered in other cases. |
michael@0 | 948 | return this.stopped + "," + this.totalBytes + "," + this.hasPartialData + |
michael@0 | 949 | "," + this.contentType; |
michael@0 | 950 | }, |
michael@0 | 951 | }; |
michael@0 | 952 | |
michael@0 | 953 | /** |
michael@0 | 954 | * Defines which properties of the Download object are serializable. |
michael@0 | 955 | */ |
michael@0 | 956 | const kSerializableDownloadProperties = [ |
michael@0 | 957 | "succeeded", |
michael@0 | 958 | "canceled", |
michael@0 | 959 | "error", |
michael@0 | 960 | "totalBytes", |
michael@0 | 961 | "hasPartialData", |
michael@0 | 962 | "tryToKeepPartialData", |
michael@0 | 963 | "launcherPath", |
michael@0 | 964 | "launchWhenSucceeded", |
michael@0 | 965 | "contentType", |
michael@0 | 966 | ]; |
michael@0 | 967 | |
michael@0 | 968 | /** |
michael@0 | 969 | * Creates a new Download object from a serializable representation. This |
michael@0 | 970 | * function is used by the createDownload method of Downloads.jsm when a new |
michael@0 | 971 | * Download object is requested, thus some properties may refer to live objects |
michael@0 | 972 | * in place of their serializable representations. |
michael@0 | 973 | * |
michael@0 | 974 | * @param aSerializable |
michael@0 | 975 | * An object with the following fields: |
michael@0 | 976 | * { |
michael@0 | 977 | * source: DownloadSource object, or its serializable representation. |
michael@0 | 978 | * See DownloadSource.fromSerializable for details. |
michael@0 | 979 | * target: DownloadTarget object, or its serializable representation. |
michael@0 | 980 | * See DownloadTarget.fromSerializable for details. |
michael@0 | 981 | * saver: Serializable representation of a DownloadSaver object. See |
michael@0 | 982 | * DownloadSaver.fromSerializable for details. If omitted, |
michael@0 | 983 | * defaults to "copy". |
michael@0 | 984 | * } |
michael@0 | 985 | * |
michael@0 | 986 | * @return The newly created Download object. |
michael@0 | 987 | */ |
michael@0 | 988 | Download.fromSerializable = function (aSerializable) { |
michael@0 | 989 | let download = new Download(); |
michael@0 | 990 | if (aSerializable.source instanceof DownloadSource) { |
michael@0 | 991 | download.source = aSerializable.source; |
michael@0 | 992 | } else { |
michael@0 | 993 | download.source = DownloadSource.fromSerializable(aSerializable.source); |
michael@0 | 994 | } |
michael@0 | 995 | if (aSerializable.target instanceof DownloadTarget) { |
michael@0 | 996 | download.target = aSerializable.target; |
michael@0 | 997 | } else { |
michael@0 | 998 | download.target = DownloadTarget.fromSerializable(aSerializable.target); |
michael@0 | 999 | } |
michael@0 | 1000 | if ("saver" in aSerializable) { |
michael@0 | 1001 | download.saver = DownloadSaver.fromSerializable(aSerializable.saver); |
michael@0 | 1002 | } else { |
michael@0 | 1003 | download.saver = DownloadSaver.fromSerializable("copy"); |
michael@0 | 1004 | } |
michael@0 | 1005 | download.saver.download = download; |
michael@0 | 1006 | |
michael@0 | 1007 | if ("startTime" in aSerializable) { |
michael@0 | 1008 | let time = aSerializable.startTime.getTime |
michael@0 | 1009 | ? aSerializable.startTime.getTime() |
michael@0 | 1010 | : aSerializable.startTime; |
michael@0 | 1011 | download.startTime = new Date(time); |
michael@0 | 1012 | } |
michael@0 | 1013 | |
michael@0 | 1014 | for (let property of kSerializableDownloadProperties) { |
michael@0 | 1015 | if (property in aSerializable) { |
michael@0 | 1016 | download[property] = aSerializable[property]; |
michael@0 | 1017 | } |
michael@0 | 1018 | } |
michael@0 | 1019 | |
michael@0 | 1020 | deserializeUnknownProperties(download, aSerializable, property => |
michael@0 | 1021 | kSerializableDownloadProperties.indexOf(property) == -1 && |
michael@0 | 1022 | property != "startTime" && |
michael@0 | 1023 | property != "source" && |
michael@0 | 1024 | property != "target" && |
michael@0 | 1025 | property != "saver"); |
michael@0 | 1026 | |
michael@0 | 1027 | return download; |
michael@0 | 1028 | }; |
michael@0 | 1029 | |
michael@0 | 1030 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1031 | //// DownloadSource |
michael@0 | 1032 | |
michael@0 | 1033 | /** |
michael@0 | 1034 | * Represents the source of a download, for example a document or an URI. |
michael@0 | 1035 | */ |
michael@0 | 1036 | this.DownloadSource = function () {} |
michael@0 | 1037 | |
michael@0 | 1038 | this.DownloadSource.prototype = { |
michael@0 | 1039 | /** |
michael@0 | 1040 | * String containing the URI for the download source. |
michael@0 | 1041 | */ |
michael@0 | 1042 | url: null, |
michael@0 | 1043 | |
michael@0 | 1044 | /** |
michael@0 | 1045 | * Indicates whether the download originated from a private window. This |
michael@0 | 1046 | * determines the context of the network request that is made to retrieve the |
michael@0 | 1047 | * resource. |
michael@0 | 1048 | */ |
michael@0 | 1049 | isPrivate: false, |
michael@0 | 1050 | |
michael@0 | 1051 | /** |
michael@0 | 1052 | * String containing the referrer URI of the download source, or null if no |
michael@0 | 1053 | * referrer should be sent or the download source is not HTTP. |
michael@0 | 1054 | */ |
michael@0 | 1055 | referrer: null, |
michael@0 | 1056 | |
michael@0 | 1057 | /** |
michael@0 | 1058 | * Returns a static representation of the current object state. |
michael@0 | 1059 | * |
michael@0 | 1060 | * @return A JavaScript object that can be serialized to JSON. |
michael@0 | 1061 | */ |
michael@0 | 1062 | toSerializable: function () |
michael@0 | 1063 | { |
michael@0 | 1064 | // Simplify the representation if we don't have other details. |
michael@0 | 1065 | if (!this.isPrivate && !this.referrer && !this._unknownProperties) { |
michael@0 | 1066 | return this.url; |
michael@0 | 1067 | } |
michael@0 | 1068 | |
michael@0 | 1069 | let serializable = { url: this.url }; |
michael@0 | 1070 | if (this.isPrivate) { |
michael@0 | 1071 | serializable.isPrivate = true; |
michael@0 | 1072 | } |
michael@0 | 1073 | if (this.referrer) { |
michael@0 | 1074 | serializable.referrer = this.referrer; |
michael@0 | 1075 | } |
michael@0 | 1076 | |
michael@0 | 1077 | serializeUnknownProperties(this, serializable); |
michael@0 | 1078 | return serializable; |
michael@0 | 1079 | }, |
michael@0 | 1080 | }; |
michael@0 | 1081 | |
michael@0 | 1082 | /** |
michael@0 | 1083 | * Creates a new DownloadSource object from its serializable representation. |
michael@0 | 1084 | * |
michael@0 | 1085 | * @param aSerializable |
michael@0 | 1086 | * Serializable representation of a DownloadSource object. This may be a |
michael@0 | 1087 | * string containing the URI for the download source, an nsIURI, or an |
michael@0 | 1088 | * object with the following properties: |
michael@0 | 1089 | * { |
michael@0 | 1090 | * url: String containing the URI for the download source. |
michael@0 | 1091 | * isPrivate: Indicates whether the download originated from a private |
michael@0 | 1092 | * window. If omitted, the download is public. |
michael@0 | 1093 | * referrer: String containing the referrer URI of the download source. |
michael@0 | 1094 | * Can be omitted or null if no referrer should be sent or |
michael@0 | 1095 | * the download source is not HTTP. |
michael@0 | 1096 | * } |
michael@0 | 1097 | * |
michael@0 | 1098 | * @return The newly created DownloadSource object. |
michael@0 | 1099 | */ |
michael@0 | 1100 | this.DownloadSource.fromSerializable = function (aSerializable) { |
michael@0 | 1101 | let source = new DownloadSource(); |
michael@0 | 1102 | if (isString(aSerializable)) { |
michael@0 | 1103 | // Convert String objects to primitive strings at this point. |
michael@0 | 1104 | source.url = aSerializable.toString(); |
michael@0 | 1105 | } else if (aSerializable instanceof Ci.nsIURI) { |
michael@0 | 1106 | source.url = aSerializable.spec; |
michael@0 | 1107 | } else { |
michael@0 | 1108 | // Convert String objects to primitive strings at this point. |
michael@0 | 1109 | source.url = aSerializable.url.toString(); |
michael@0 | 1110 | if ("isPrivate" in aSerializable) { |
michael@0 | 1111 | source.isPrivate = aSerializable.isPrivate; |
michael@0 | 1112 | } |
michael@0 | 1113 | if ("referrer" in aSerializable) { |
michael@0 | 1114 | source.referrer = aSerializable.referrer; |
michael@0 | 1115 | } |
michael@0 | 1116 | |
michael@0 | 1117 | deserializeUnknownProperties(source, aSerializable, property => |
michael@0 | 1118 | property != "url" && property != "isPrivate" && property != "referrer"); |
michael@0 | 1119 | } |
michael@0 | 1120 | |
michael@0 | 1121 | return source; |
michael@0 | 1122 | }; |
michael@0 | 1123 | |
michael@0 | 1124 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1125 | //// DownloadTarget |
michael@0 | 1126 | |
michael@0 | 1127 | /** |
michael@0 | 1128 | * Represents the target of a download, for example a file in the global |
michael@0 | 1129 | * downloads directory, or a file in the system temporary directory. |
michael@0 | 1130 | */ |
michael@0 | 1131 | this.DownloadTarget = function () {} |
michael@0 | 1132 | |
michael@0 | 1133 | this.DownloadTarget.prototype = { |
michael@0 | 1134 | /** |
michael@0 | 1135 | * String containing the path of the target file. |
michael@0 | 1136 | */ |
michael@0 | 1137 | path: null, |
michael@0 | 1138 | |
michael@0 | 1139 | /** |
michael@0 | 1140 | * String containing the path of the ".part" file containing the data |
michael@0 | 1141 | * downloaded so far, or null to disable the use of a ".part" file to keep |
michael@0 | 1142 | * partially downloaded data. |
michael@0 | 1143 | */ |
michael@0 | 1144 | partFilePath: null, |
michael@0 | 1145 | |
michael@0 | 1146 | /** |
michael@0 | 1147 | * Returns a static representation of the current object state. |
michael@0 | 1148 | * |
michael@0 | 1149 | * @return A JavaScript object that can be serialized to JSON. |
michael@0 | 1150 | */ |
michael@0 | 1151 | toSerializable: function () |
michael@0 | 1152 | { |
michael@0 | 1153 | // Simplify the representation if we don't have other details. |
michael@0 | 1154 | if (!this.partFilePath && !this._unknownProperties) { |
michael@0 | 1155 | return this.path; |
michael@0 | 1156 | } |
michael@0 | 1157 | |
michael@0 | 1158 | let serializable = { path: this.path, |
michael@0 | 1159 | partFilePath: this.partFilePath }; |
michael@0 | 1160 | serializeUnknownProperties(this, serializable); |
michael@0 | 1161 | return serializable; |
michael@0 | 1162 | }, |
michael@0 | 1163 | }; |
michael@0 | 1164 | |
michael@0 | 1165 | /** |
michael@0 | 1166 | * Creates a new DownloadTarget object from its serializable representation. |
michael@0 | 1167 | * |
michael@0 | 1168 | * @param aSerializable |
michael@0 | 1169 | * Serializable representation of a DownloadTarget object. This may be a |
michael@0 | 1170 | * string containing the path of the target file, an nsIFile, or an |
michael@0 | 1171 | * object with the following properties: |
michael@0 | 1172 | * { |
michael@0 | 1173 | * path: String containing the path of the target file. |
michael@0 | 1174 | * partFilePath: optional string containing the part file path. |
michael@0 | 1175 | * } |
michael@0 | 1176 | * |
michael@0 | 1177 | * @return The newly created DownloadTarget object. |
michael@0 | 1178 | */ |
michael@0 | 1179 | this.DownloadTarget.fromSerializable = function (aSerializable) { |
michael@0 | 1180 | let target = new DownloadTarget(); |
michael@0 | 1181 | if (isString(aSerializable)) { |
michael@0 | 1182 | // Convert String objects to primitive strings at this point. |
michael@0 | 1183 | target.path = aSerializable.toString(); |
michael@0 | 1184 | } else if (aSerializable instanceof Ci.nsIFile) { |
michael@0 | 1185 | // Read the "path" property of nsIFile after checking the object type. |
michael@0 | 1186 | target.path = aSerializable.path; |
michael@0 | 1187 | } else { |
michael@0 | 1188 | // Read the "path" property of the serializable DownloadTarget |
michael@0 | 1189 | // representation, converting String objects to primitive strings. |
michael@0 | 1190 | target.path = aSerializable.path.toString(); |
michael@0 | 1191 | if ("partFilePath" in aSerializable) { |
michael@0 | 1192 | target.partFilePath = aSerializable.partFilePath; |
michael@0 | 1193 | } |
michael@0 | 1194 | |
michael@0 | 1195 | deserializeUnknownProperties(target, aSerializable, property => |
michael@0 | 1196 | property != "path" && property != "partFilePath"); |
michael@0 | 1197 | } |
michael@0 | 1198 | return target; |
michael@0 | 1199 | }; |
michael@0 | 1200 | |
michael@0 | 1201 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1202 | //// DownloadError |
michael@0 | 1203 | |
michael@0 | 1204 | /** |
michael@0 | 1205 | * Provides detailed information about a download failure. |
michael@0 | 1206 | * |
michael@0 | 1207 | * @param aProperties |
michael@0 | 1208 | * Object which may contain any of the following properties: |
michael@0 | 1209 | * { |
michael@0 | 1210 | * result: Result error code, defaulting to Cr.NS_ERROR_FAILURE |
michael@0 | 1211 | * message: String error message to be displayed, or null to use the |
michael@0 | 1212 | * message associated with the result code. |
michael@0 | 1213 | * inferCause: If true, attempts to determine if the cause of the |
michael@0 | 1214 | * download is a network failure or a local file failure, |
michael@0 | 1215 | * based on a set of known values of the result code. |
michael@0 | 1216 | * This is useful when the error is received by a |
michael@0 | 1217 | * component that handles both aspects of the download. |
michael@0 | 1218 | * } |
michael@0 | 1219 | * The properties object may also contain any of the DownloadError's |
michael@0 | 1220 | * because properties, which will be set accordingly in the error object. |
michael@0 | 1221 | */ |
michael@0 | 1222 | this.DownloadError = function (aProperties) |
michael@0 | 1223 | { |
michael@0 | 1224 | const NS_ERROR_MODULE_BASE_OFFSET = 0x45; |
michael@0 | 1225 | const NS_ERROR_MODULE_NETWORK = 6; |
michael@0 | 1226 | const NS_ERROR_MODULE_FILES = 13; |
michael@0 | 1227 | |
michael@0 | 1228 | // Set the error name used by the Error object prototype first. |
michael@0 | 1229 | this.name = "DownloadError"; |
michael@0 | 1230 | this.result = aProperties.result || Cr.NS_ERROR_FAILURE; |
michael@0 | 1231 | if (aProperties.message) { |
michael@0 | 1232 | this.message = aProperties.message; |
michael@0 | 1233 | } else if (aProperties.becauseBlocked || |
michael@0 | 1234 | aProperties.becauseBlockedByParentalControls || |
michael@0 | 1235 | aProperties.becauseBlockedByReputationCheck) { |
michael@0 | 1236 | this.message = "Download blocked."; |
michael@0 | 1237 | } else { |
michael@0 | 1238 | let exception = new Components.Exception("", this.result); |
michael@0 | 1239 | this.message = exception.toString(); |
michael@0 | 1240 | } |
michael@0 | 1241 | if (aProperties.inferCause) { |
michael@0 | 1242 | let module = ((this.result & 0x7FFF0000) >> 16) - |
michael@0 | 1243 | NS_ERROR_MODULE_BASE_OFFSET; |
michael@0 | 1244 | this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK); |
michael@0 | 1245 | this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES); |
michael@0 | 1246 | } |
michael@0 | 1247 | else { |
michael@0 | 1248 | if (aProperties.becauseSourceFailed) { |
michael@0 | 1249 | this.becauseSourceFailed = true; |
michael@0 | 1250 | } |
michael@0 | 1251 | if (aProperties.becauseTargetFailed) { |
michael@0 | 1252 | this.becauseTargetFailed = true; |
michael@0 | 1253 | } |
michael@0 | 1254 | } |
michael@0 | 1255 | |
michael@0 | 1256 | if (aProperties.becauseBlockedByParentalControls) { |
michael@0 | 1257 | this.becauseBlocked = true; |
michael@0 | 1258 | this.becauseBlockedByParentalControls = true; |
michael@0 | 1259 | } else if (aProperties.becauseBlockedByReputationCheck) { |
michael@0 | 1260 | this.becauseBlocked = true; |
michael@0 | 1261 | this.becauseBlockedByReputationCheck = true; |
michael@0 | 1262 | } else if (aProperties.becauseBlocked) { |
michael@0 | 1263 | this.becauseBlocked = true; |
michael@0 | 1264 | } |
michael@0 | 1265 | |
michael@0 | 1266 | this.stack = new Error().stack; |
michael@0 | 1267 | } |
michael@0 | 1268 | |
michael@0 | 1269 | this.DownloadError.prototype = { |
michael@0 | 1270 | __proto__: Error.prototype, |
michael@0 | 1271 | |
michael@0 | 1272 | /** |
michael@0 | 1273 | * The result code associated with this error. |
michael@0 | 1274 | */ |
michael@0 | 1275 | result: false, |
michael@0 | 1276 | |
michael@0 | 1277 | /** |
michael@0 | 1278 | * Indicates an error occurred while reading from the remote location. |
michael@0 | 1279 | */ |
michael@0 | 1280 | becauseSourceFailed: false, |
michael@0 | 1281 | |
michael@0 | 1282 | /** |
michael@0 | 1283 | * Indicates an error occurred while writing to the local target. |
michael@0 | 1284 | */ |
michael@0 | 1285 | becauseTargetFailed: false, |
michael@0 | 1286 | |
michael@0 | 1287 | /** |
michael@0 | 1288 | * Indicates the download failed because it was blocked. If the reason for |
michael@0 | 1289 | * blocking is known, the corresponding property will be also set. |
michael@0 | 1290 | */ |
michael@0 | 1291 | becauseBlocked: false, |
michael@0 | 1292 | |
michael@0 | 1293 | /** |
michael@0 | 1294 | * Indicates the download was blocked because downloads are globally |
michael@0 | 1295 | * disallowed by the Parental Controls or Family Safety features on Windows. |
michael@0 | 1296 | */ |
michael@0 | 1297 | becauseBlockedByParentalControls: false, |
michael@0 | 1298 | |
michael@0 | 1299 | /** |
michael@0 | 1300 | * Indicates the download was blocked because it failed the reputation check |
michael@0 | 1301 | * and may be malware. |
michael@0 | 1302 | */ |
michael@0 | 1303 | becauseBlockedByReputationCheck: false, |
michael@0 | 1304 | }; |
michael@0 | 1305 | |
michael@0 | 1306 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1307 | //// DownloadSaver |
michael@0 | 1308 | |
michael@0 | 1309 | /** |
michael@0 | 1310 | * Template for an object that actually transfers the data for the download. |
michael@0 | 1311 | */ |
michael@0 | 1312 | this.DownloadSaver = function () {} |
michael@0 | 1313 | |
michael@0 | 1314 | this.DownloadSaver.prototype = { |
michael@0 | 1315 | /** |
michael@0 | 1316 | * Download object for raising notifications and reading properties. |
michael@0 | 1317 | * |
michael@0 | 1318 | * If the tryToKeepPartialData property of the download object is false, the |
michael@0 | 1319 | * saver should never try to keep partially downloaded data if the download |
michael@0 | 1320 | * fails. |
michael@0 | 1321 | */ |
michael@0 | 1322 | download: null, |
michael@0 | 1323 | |
michael@0 | 1324 | /** |
michael@0 | 1325 | * Executes the download. |
michael@0 | 1326 | * |
michael@0 | 1327 | * @param aSetProgressBytesFn |
michael@0 | 1328 | * This function may be called by the saver to report progress. It |
michael@0 | 1329 | * takes three arguments: the first is the number of bytes transferred |
michael@0 | 1330 | * until now, the second is the total number of bytes to be |
michael@0 | 1331 | * transferred (or -1 if unknown), the third indicates whether the |
michael@0 | 1332 | * partially downloaded data can be used when restarting the download |
michael@0 | 1333 | * if it fails or is canceled. |
michael@0 | 1334 | * @param aSetPropertiesFn |
michael@0 | 1335 | * This function may be called by the saver to report information |
michael@0 | 1336 | * about new download properties discovered by the saver during the |
michael@0 | 1337 | * download process. It takes an object where the keys represents |
michael@0 | 1338 | * the names of the properties to set, and the value represents the |
michael@0 | 1339 | * value to set. |
michael@0 | 1340 | * |
michael@0 | 1341 | * @return {Promise} |
michael@0 | 1342 | * @resolves When the download has finished successfully. |
michael@0 | 1343 | * @rejects JavaScript exception if the download failed. |
michael@0 | 1344 | */ |
michael@0 | 1345 | execute: function DS_execute(aSetProgressBytesFn, aSetPropertiesFn) |
michael@0 | 1346 | { |
michael@0 | 1347 | throw new Error("Not implemented."); |
michael@0 | 1348 | }, |
michael@0 | 1349 | |
michael@0 | 1350 | /** |
michael@0 | 1351 | * Cancels the download. |
michael@0 | 1352 | */ |
michael@0 | 1353 | cancel: function DS_cancel() |
michael@0 | 1354 | { |
michael@0 | 1355 | throw new Error("Not implemented."); |
michael@0 | 1356 | }, |
michael@0 | 1357 | |
michael@0 | 1358 | /** |
michael@0 | 1359 | * Removes any partial data kept as part of a canceled or failed download. |
michael@0 | 1360 | * |
michael@0 | 1361 | * This method is never called until the promise returned by "execute" is |
michael@0 | 1362 | * either resolved or rejected, and the "execute" method is not called again |
michael@0 | 1363 | * until the promise returned by this method is resolved or rejected. |
michael@0 | 1364 | * |
michael@0 | 1365 | * @return {Promise} |
michael@0 | 1366 | * @resolves When the operation has finished successfully. |
michael@0 | 1367 | * @rejects JavaScript exception. |
michael@0 | 1368 | */ |
michael@0 | 1369 | removePartialData: function DS_removePartialData() |
michael@0 | 1370 | { |
michael@0 | 1371 | return Promise.resolve(); |
michael@0 | 1372 | }, |
michael@0 | 1373 | |
michael@0 | 1374 | /** |
michael@0 | 1375 | * This can be called by the saver implementation when the download is already |
michael@0 | 1376 | * started, to add it to the browsing history. This method has no effect if |
michael@0 | 1377 | * the download is private. |
michael@0 | 1378 | */ |
michael@0 | 1379 | addToHistory: function () |
michael@0 | 1380 | { |
michael@0 | 1381 | if (this.download.source.isPrivate) { |
michael@0 | 1382 | return; |
michael@0 | 1383 | } |
michael@0 | 1384 | |
michael@0 | 1385 | let sourceUri = NetUtil.newURI(this.download.source.url); |
michael@0 | 1386 | let referrer = this.download.source.referrer; |
michael@0 | 1387 | let referrerUri = referrer ? NetUtil.newURI(referrer) : null; |
michael@0 | 1388 | let targetUri = NetUtil.newURI(new FileUtils.File( |
michael@0 | 1389 | this.download.target.path)); |
michael@0 | 1390 | |
michael@0 | 1391 | // The start time is always available when we reach this point. |
michael@0 | 1392 | let startPRTime = this.download.startTime.getTime() * 1000; |
michael@0 | 1393 | |
michael@0 | 1394 | gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime, |
michael@0 | 1395 | targetUri); |
michael@0 | 1396 | }, |
michael@0 | 1397 | |
michael@0 | 1398 | /** |
michael@0 | 1399 | * Returns a static representation of the current object state. |
michael@0 | 1400 | * |
michael@0 | 1401 | * @return A JavaScript object that can be serialized to JSON. |
michael@0 | 1402 | */ |
michael@0 | 1403 | toSerializable: function () |
michael@0 | 1404 | { |
michael@0 | 1405 | throw new Error("Not implemented."); |
michael@0 | 1406 | }, |
michael@0 | 1407 | |
michael@0 | 1408 | /** |
michael@0 | 1409 | * Returns the SHA-256 hash of the downloaded file, if it exists. |
michael@0 | 1410 | */ |
michael@0 | 1411 | getSha256Hash: function () |
michael@0 | 1412 | { |
michael@0 | 1413 | throw new Error("Not implemented."); |
michael@0 | 1414 | }, |
michael@0 | 1415 | |
michael@0 | 1416 | getSignatureInfo: function () |
michael@0 | 1417 | { |
michael@0 | 1418 | throw new Error("Not implemented."); |
michael@0 | 1419 | }, |
michael@0 | 1420 | }; // DownloadSaver |
michael@0 | 1421 | |
michael@0 | 1422 | /** |
michael@0 | 1423 | * Creates a new DownloadSaver object from its serializable representation. |
michael@0 | 1424 | * |
michael@0 | 1425 | * @param aSerializable |
michael@0 | 1426 | * Serializable representation of a DownloadSaver object. If no initial |
michael@0 | 1427 | * state information for the saver object is needed, can be a string |
michael@0 | 1428 | * representing the class of the download operation, for example "copy". |
michael@0 | 1429 | * |
michael@0 | 1430 | * @return The newly created DownloadSaver object. |
michael@0 | 1431 | */ |
michael@0 | 1432 | this.DownloadSaver.fromSerializable = function (aSerializable) { |
michael@0 | 1433 | let serializable = isString(aSerializable) ? { type: aSerializable } |
michael@0 | 1434 | : aSerializable; |
michael@0 | 1435 | let saver; |
michael@0 | 1436 | switch (serializable.type) { |
michael@0 | 1437 | case "copy": |
michael@0 | 1438 | saver = DownloadCopySaver.fromSerializable(serializable); |
michael@0 | 1439 | break; |
michael@0 | 1440 | case "legacy": |
michael@0 | 1441 | saver = DownloadLegacySaver.fromSerializable(serializable); |
michael@0 | 1442 | break; |
michael@0 | 1443 | default: |
michael@0 | 1444 | throw new Error("Unrecoginzed download saver type."); |
michael@0 | 1445 | } |
michael@0 | 1446 | return saver; |
michael@0 | 1447 | }; |
michael@0 | 1448 | |
michael@0 | 1449 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1450 | //// DownloadCopySaver |
michael@0 | 1451 | |
michael@0 | 1452 | /** |
michael@0 | 1453 | * Saver object that simply copies the entire source file to the target. |
michael@0 | 1454 | */ |
michael@0 | 1455 | this.DownloadCopySaver = function () {} |
michael@0 | 1456 | |
michael@0 | 1457 | this.DownloadCopySaver.prototype = { |
michael@0 | 1458 | __proto__: DownloadSaver.prototype, |
michael@0 | 1459 | |
michael@0 | 1460 | /** |
michael@0 | 1461 | * BackgroundFileSaver object currently handling the download. |
michael@0 | 1462 | */ |
michael@0 | 1463 | _backgroundFileSaver: null, |
michael@0 | 1464 | |
michael@0 | 1465 | /** |
michael@0 | 1466 | * Indicates whether the "cancel" method has been called. This is used to |
michael@0 | 1467 | * prevent the request from starting in case the operation is canceled before |
michael@0 | 1468 | * the BackgroundFileSaver instance has been created. |
michael@0 | 1469 | */ |
michael@0 | 1470 | _canceled: false, |
michael@0 | 1471 | |
michael@0 | 1472 | /** |
michael@0 | 1473 | * Save the SHA-256 hash in raw bytes of the downloaded file. This is null |
michael@0 | 1474 | * unless BackgroundFileSaver has successfully completed saving the file. |
michael@0 | 1475 | */ |
michael@0 | 1476 | _sha256Hash: null, |
michael@0 | 1477 | |
michael@0 | 1478 | /** |
michael@0 | 1479 | * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert |
michael@0 | 1480 | * if the file is signed. This is empty if the file is unsigned, and null |
michael@0 | 1481 | * unless BackgroundFileSaver has successfully completed saving the file. |
michael@0 | 1482 | */ |
michael@0 | 1483 | _signatureInfo: null, |
michael@0 | 1484 | |
michael@0 | 1485 | /** |
michael@0 | 1486 | * True if the associated download has already been added to browsing history. |
michael@0 | 1487 | */ |
michael@0 | 1488 | alreadyAddedToHistory: false, |
michael@0 | 1489 | |
michael@0 | 1490 | /** |
michael@0 | 1491 | * String corresponding to the entityID property of the nsIResumableChannel |
michael@0 | 1492 | * used to execute the download, or null if the channel was not resumable or |
michael@0 | 1493 | * the saver was instructed not to keep partially downloaded data. |
michael@0 | 1494 | */ |
michael@0 | 1495 | entityID: null, |
michael@0 | 1496 | |
michael@0 | 1497 | /** |
michael@0 | 1498 | * Implements "DownloadSaver.execute". |
michael@0 | 1499 | */ |
michael@0 | 1500 | execute: function DCS_execute(aSetProgressBytesFn, aSetPropertiesFn) |
michael@0 | 1501 | { |
michael@0 | 1502 | let copySaver = this; |
michael@0 | 1503 | |
michael@0 | 1504 | this._canceled = false; |
michael@0 | 1505 | |
michael@0 | 1506 | let download = this.download; |
michael@0 | 1507 | let targetPath = download.target.path; |
michael@0 | 1508 | let partFilePath = download.target.partFilePath; |
michael@0 | 1509 | let keepPartialData = download.tryToKeepPartialData; |
michael@0 | 1510 | |
michael@0 | 1511 | return Task.spawn(function task_DCS_execute() { |
michael@0 | 1512 | // Add the download to history the first time it is started in this |
michael@0 | 1513 | // session. If the download is restarted in a different session, a new |
michael@0 | 1514 | // history visit will be added. We do this just to avoid the complexity |
michael@0 | 1515 | // of serializing this state between sessions, since adding a new visit |
michael@0 | 1516 | // does not have any noticeable side effect. |
michael@0 | 1517 | if (!this.alreadyAddedToHistory) { |
michael@0 | 1518 | this.addToHistory(); |
michael@0 | 1519 | this.alreadyAddedToHistory = true; |
michael@0 | 1520 | } |
michael@0 | 1521 | |
michael@0 | 1522 | // To reduce the chance that other downloads reuse the same final target |
michael@0 | 1523 | // file name, we should create a placeholder as soon as possible, before |
michael@0 | 1524 | // starting the network request. The placeholder is also required in case |
michael@0 | 1525 | // we are using a ".part" file instead of the final target while the |
michael@0 | 1526 | // download is in progress. |
michael@0 | 1527 | try { |
michael@0 | 1528 | // If the file already exists, don't delete its contents yet. |
michael@0 | 1529 | let file = yield OS.File.open(targetPath, { write: true }); |
michael@0 | 1530 | yield file.close(); |
michael@0 | 1531 | } catch (ex if ex instanceof OS.File.Error) { |
michael@0 | 1532 | // Throw a DownloadError indicating that the operation failed because of |
michael@0 | 1533 | // the target file. We cannot translate this into a specific result |
michael@0 | 1534 | // code, but we preserve the original message using the toString method. |
michael@0 | 1535 | let error = new DownloadError({ message: ex.toString() }); |
michael@0 | 1536 | error.becauseTargetFailed = true; |
michael@0 | 1537 | throw error; |
michael@0 | 1538 | } |
michael@0 | 1539 | |
michael@0 | 1540 | try { |
michael@0 | 1541 | let deferSaveComplete = Promise.defer(); |
michael@0 | 1542 | |
michael@0 | 1543 | if (this._canceled) { |
michael@0 | 1544 | // Don't create the BackgroundFileSaver object if we have been |
michael@0 | 1545 | // canceled meanwhile. |
michael@0 | 1546 | throw new DownloadError({ message: "Saver canceled." }); |
michael@0 | 1547 | } |
michael@0 | 1548 | |
michael@0 | 1549 | // Create the object that will save the file in a background thread. |
michael@0 | 1550 | let backgroundFileSaver = new BackgroundFileSaverStreamListener(); |
michael@0 | 1551 | try { |
michael@0 | 1552 | // When the operation completes, reflect the status in the promise |
michael@0 | 1553 | // returned by this download execution function. |
michael@0 | 1554 | backgroundFileSaver.observer = { |
michael@0 | 1555 | onTargetChange: function () { }, |
michael@0 | 1556 | onSaveComplete: (aSaver, aStatus) => { |
michael@0 | 1557 | // Send notifications now that we can restart if needed. |
michael@0 | 1558 | if (Components.isSuccessCode(aStatus)) { |
michael@0 | 1559 | // Save the hash before freeing backgroundFileSaver. |
michael@0 | 1560 | this._sha256Hash = aSaver.sha256Hash; |
michael@0 | 1561 | this._signatureInfo = aSaver.signatureInfo; |
michael@0 | 1562 | deferSaveComplete.resolve(); |
michael@0 | 1563 | } else { |
michael@0 | 1564 | // Infer the origin of the error from the failure code, because |
michael@0 | 1565 | // BackgroundFileSaver does not provide more specific data. |
michael@0 | 1566 | let properties = { result: aStatus, inferCause: true }; |
michael@0 | 1567 | deferSaveComplete.reject(new DownloadError(properties)); |
michael@0 | 1568 | } |
michael@0 | 1569 | // Free the reference cycle, to release resources earlier. |
michael@0 | 1570 | backgroundFileSaver.observer = null; |
michael@0 | 1571 | this._backgroundFileSaver = null; |
michael@0 | 1572 | }, |
michael@0 | 1573 | }; |
michael@0 | 1574 | |
michael@0 | 1575 | // Create a channel from the source, and listen to progress |
michael@0 | 1576 | // notifications. |
michael@0 | 1577 | let channel = NetUtil.newChannel(NetUtil.newURI(download.source.url)); |
michael@0 | 1578 | if (channel instanceof Ci.nsIPrivateBrowsingChannel) { |
michael@0 | 1579 | channel.setPrivate(download.source.isPrivate); |
michael@0 | 1580 | } |
michael@0 | 1581 | if (channel instanceof Ci.nsIHttpChannel && |
michael@0 | 1582 | download.source.referrer) { |
michael@0 | 1583 | channel.referrer = NetUtil.newURI(download.source.referrer); |
michael@0 | 1584 | } |
michael@0 | 1585 | |
michael@0 | 1586 | // If we have data that we can use to resume the download from where |
michael@0 | 1587 | // it stopped, try to use it. |
michael@0 | 1588 | let resumeAttempted = false; |
michael@0 | 1589 | let resumeFromBytes = 0; |
michael@0 | 1590 | if (channel instanceof Ci.nsIResumableChannel && this.entityID && |
michael@0 | 1591 | partFilePath && keepPartialData) { |
michael@0 | 1592 | try { |
michael@0 | 1593 | let stat = yield OS.File.stat(partFilePath); |
michael@0 | 1594 | channel.resumeAt(stat.size, this.entityID); |
michael@0 | 1595 | resumeAttempted = true; |
michael@0 | 1596 | resumeFromBytes = stat.size; |
michael@0 | 1597 | } catch (ex if ex instanceof OS.File.Error && |
michael@0 | 1598 | ex.becauseNoSuchFile) { } |
michael@0 | 1599 | } |
michael@0 | 1600 | |
michael@0 | 1601 | channel.notificationCallbacks = { |
michael@0 | 1602 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]), |
michael@0 | 1603 | getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]), |
michael@0 | 1604 | onProgress: function DCSE_onProgress(aRequest, aContext, aProgress, |
michael@0 | 1605 | aProgressMax) |
michael@0 | 1606 | { |
michael@0 | 1607 | let currentBytes = resumeFromBytes + aProgress; |
michael@0 | 1608 | let totalBytes = aProgressMax == -1 ? -1 : (resumeFromBytes + |
michael@0 | 1609 | aProgressMax); |
michael@0 | 1610 | aSetProgressBytesFn(currentBytes, totalBytes, aProgress > 0 && |
michael@0 | 1611 | partFilePath && keepPartialData); |
michael@0 | 1612 | }, |
michael@0 | 1613 | onStatus: function () { }, |
michael@0 | 1614 | }; |
michael@0 | 1615 | |
michael@0 | 1616 | // Open the channel, directing output to the background file saver. |
michael@0 | 1617 | backgroundFileSaver.QueryInterface(Ci.nsIStreamListener); |
michael@0 | 1618 | channel.asyncOpen({ |
michael@0 | 1619 | onStartRequest: function (aRequest, aContext) { |
michael@0 | 1620 | backgroundFileSaver.onStartRequest(aRequest, aContext); |
michael@0 | 1621 | |
michael@0 | 1622 | // Check if the request's response has been blocked by Windows |
michael@0 | 1623 | // Parental Controls with an HTTP 450 error code. |
michael@0 | 1624 | if (aRequest instanceof Ci.nsIHttpChannel && |
michael@0 | 1625 | aRequest.responseStatus == 450) { |
michael@0 | 1626 | // Set a flag that can be retrieved later when handling the |
michael@0 | 1627 | // cancellation so that the proper error can be thrown. |
michael@0 | 1628 | this.download._blockedByParentalControls = true; |
michael@0 | 1629 | aRequest.cancel(Cr.NS_BINDING_ABORTED); |
michael@0 | 1630 | return; |
michael@0 | 1631 | } |
michael@0 | 1632 | |
michael@0 | 1633 | aSetPropertiesFn({ contentType: channel.contentType }); |
michael@0 | 1634 | |
michael@0 | 1635 | // Ensure we report the value of "Content-Length", if available, |
michael@0 | 1636 | // even if the download doesn't generate any progress events |
michael@0 | 1637 | // later. |
michael@0 | 1638 | if (channel.contentLength >= 0) { |
michael@0 | 1639 | aSetProgressBytesFn(0, channel.contentLength); |
michael@0 | 1640 | } |
michael@0 | 1641 | |
michael@0 | 1642 | // If the URL we are downloading from includes a file extension |
michael@0 | 1643 | // that matches the "Content-Encoding" header, for example ".gz" |
michael@0 | 1644 | // with a "gzip" encoding, we should save the file in its encoded |
michael@0 | 1645 | // form. In all other cases, we decode the body while saving. |
michael@0 | 1646 | if (channel instanceof Ci.nsIEncodedChannel && |
michael@0 | 1647 | channel.contentEncodings) { |
michael@0 | 1648 | let uri = channel.URI; |
michael@0 | 1649 | if (uri instanceof Ci.nsIURL && uri.fileExtension) { |
michael@0 | 1650 | // Only the first, outermost encoding is considered. |
michael@0 | 1651 | let encoding = channel.contentEncodings.getNext(); |
michael@0 | 1652 | if (encoding) { |
michael@0 | 1653 | channel.applyConversion = |
michael@0 | 1654 | gExternalHelperAppService.applyDecodingForExtension( |
michael@0 | 1655 | uri.fileExtension, encoding); |
michael@0 | 1656 | } |
michael@0 | 1657 | } |
michael@0 | 1658 | } |
michael@0 | 1659 | |
michael@0 | 1660 | if (keepPartialData) { |
michael@0 | 1661 | // If the source is not resumable, don't keep partial data even |
michael@0 | 1662 | // if we were asked to try and do it. |
michael@0 | 1663 | if (aRequest instanceof Ci.nsIResumableChannel) { |
michael@0 | 1664 | try { |
michael@0 | 1665 | // If reading the ID succeeds, the source is resumable. |
michael@0 | 1666 | this.entityID = aRequest.entityID; |
michael@0 | 1667 | } catch (ex if ex instanceof Components.Exception && |
michael@0 | 1668 | ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { |
michael@0 | 1669 | keepPartialData = false; |
michael@0 | 1670 | } |
michael@0 | 1671 | } else { |
michael@0 | 1672 | keepPartialData = false; |
michael@0 | 1673 | } |
michael@0 | 1674 | } |
michael@0 | 1675 | |
michael@0 | 1676 | // Enable hashing and signature verification before setting the |
michael@0 | 1677 | // target. |
michael@0 | 1678 | backgroundFileSaver.enableSha256(); |
michael@0 | 1679 | backgroundFileSaver.enableSignatureInfo(); |
michael@0 | 1680 | if (partFilePath) { |
michael@0 | 1681 | // If we actually resumed a request, append to the partial data. |
michael@0 | 1682 | if (resumeAttempted) { |
michael@0 | 1683 | // TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED |
michael@0 | 1684 | backgroundFileSaver.enableAppend(); |
michael@0 | 1685 | } |
michael@0 | 1686 | |
michael@0 | 1687 | // Use a part file, determining if we should keep it on failure. |
michael@0 | 1688 | backgroundFileSaver.setTarget(new FileUtils.File(partFilePath), |
michael@0 | 1689 | keepPartialData); |
michael@0 | 1690 | } else { |
michael@0 | 1691 | // Set the final target file, and delete it on failure. |
michael@0 | 1692 | backgroundFileSaver.setTarget(new FileUtils.File(targetPath), |
michael@0 | 1693 | false); |
michael@0 | 1694 | } |
michael@0 | 1695 | }.bind(copySaver), |
michael@0 | 1696 | |
michael@0 | 1697 | onStopRequest: function (aRequest, aContext, aStatusCode) { |
michael@0 | 1698 | try { |
michael@0 | 1699 | backgroundFileSaver.onStopRequest(aRequest, aContext, |
michael@0 | 1700 | aStatusCode); |
michael@0 | 1701 | } finally { |
michael@0 | 1702 | // If the data transfer completed successfully, indicate to the |
michael@0 | 1703 | // background file saver that the operation can finish. If the |
michael@0 | 1704 | // data transfer failed, the saver has been already stopped. |
michael@0 | 1705 | if (Components.isSuccessCode(aStatusCode)) { |
michael@0 | 1706 | if (partFilePath) { |
michael@0 | 1707 | // Move to the final target if we were using a part file. |
michael@0 | 1708 | backgroundFileSaver.setTarget( |
michael@0 | 1709 | new FileUtils.File(targetPath), false); |
michael@0 | 1710 | } |
michael@0 | 1711 | backgroundFileSaver.finish(Cr.NS_OK); |
michael@0 | 1712 | } |
michael@0 | 1713 | } |
michael@0 | 1714 | }.bind(copySaver), |
michael@0 | 1715 | |
michael@0 | 1716 | onDataAvailable: function (aRequest, aContext, aInputStream, |
michael@0 | 1717 | aOffset, aCount) { |
michael@0 | 1718 | backgroundFileSaver.onDataAvailable(aRequest, aContext, |
michael@0 | 1719 | aInputStream, aOffset, |
michael@0 | 1720 | aCount); |
michael@0 | 1721 | }.bind(copySaver), |
michael@0 | 1722 | }, null); |
michael@0 | 1723 | |
michael@0 | 1724 | // We should check if we have been canceled in the meantime, after |
michael@0 | 1725 | // all the previous asynchronous operations have been executed and |
michael@0 | 1726 | // just before we set the _backgroundFileSaver property. |
michael@0 | 1727 | if (this._canceled) { |
michael@0 | 1728 | throw new DownloadError({ message: "Saver canceled." }); |
michael@0 | 1729 | } |
michael@0 | 1730 | |
michael@0 | 1731 | // If the operation succeeded, store the object to allow cancellation. |
michael@0 | 1732 | this._backgroundFileSaver = backgroundFileSaver; |
michael@0 | 1733 | } catch (ex) { |
michael@0 | 1734 | // In case an error occurs while setting up the chain of objects for |
michael@0 | 1735 | // the download, ensure that we release the resources of the saver. |
michael@0 | 1736 | backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE); |
michael@0 | 1737 | throw ex; |
michael@0 | 1738 | } |
michael@0 | 1739 | |
michael@0 | 1740 | // We will wait on this promise in case no error occurred while setting |
michael@0 | 1741 | // up the chain of objects for the download. |
michael@0 | 1742 | yield deferSaveComplete.promise; |
michael@0 | 1743 | } catch (ex) { |
michael@0 | 1744 | // Ensure we always remove the placeholder for the final target file on |
michael@0 | 1745 | // failure, independently of which code path failed. In some cases, the |
michael@0 | 1746 | // background file saver may have already removed the file. |
michael@0 | 1747 | try { |
michael@0 | 1748 | yield OS.File.remove(targetPath); |
michael@0 | 1749 | } catch (e2) { |
michael@0 | 1750 | // If we failed during the operation, we report the error but use the |
michael@0 | 1751 | // original one as the failure reason of the download. Note that on |
michael@0 | 1752 | // Windows we may get an access denied error instead of a no such file |
michael@0 | 1753 | // error if the file existed before, and was recently deleted. |
michael@0 | 1754 | if (!(e2 instanceof OS.File.Error && |
michael@0 | 1755 | (e2.becauseNoSuchFile || e2.becauseAccessDenied))) { |
michael@0 | 1756 | Cu.reportError(e2); |
michael@0 | 1757 | } |
michael@0 | 1758 | } |
michael@0 | 1759 | throw ex; |
michael@0 | 1760 | } |
michael@0 | 1761 | }.bind(this)); |
michael@0 | 1762 | }, |
michael@0 | 1763 | |
michael@0 | 1764 | /** |
michael@0 | 1765 | * Implements "DownloadSaver.cancel". |
michael@0 | 1766 | */ |
michael@0 | 1767 | cancel: function DCS_cancel() |
michael@0 | 1768 | { |
michael@0 | 1769 | this._canceled = true; |
michael@0 | 1770 | if (this._backgroundFileSaver) { |
michael@0 | 1771 | this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE); |
michael@0 | 1772 | this._backgroundFileSaver = null; |
michael@0 | 1773 | } |
michael@0 | 1774 | }, |
michael@0 | 1775 | |
michael@0 | 1776 | /** |
michael@0 | 1777 | * Implements "DownloadSaver.removePartialData". |
michael@0 | 1778 | */ |
michael@0 | 1779 | removePartialData: function () |
michael@0 | 1780 | { |
michael@0 | 1781 | return Task.spawn(function task_DCS_removePartialData() { |
michael@0 | 1782 | if (this.download.target.partFilePath) { |
michael@0 | 1783 | try { |
michael@0 | 1784 | yield OS.File.remove(this.download.target.partFilePath); |
michael@0 | 1785 | } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { } |
michael@0 | 1786 | } |
michael@0 | 1787 | }.bind(this)); |
michael@0 | 1788 | }, |
michael@0 | 1789 | |
michael@0 | 1790 | /** |
michael@0 | 1791 | * Implements "DownloadSaver.toSerializable". |
michael@0 | 1792 | */ |
michael@0 | 1793 | toSerializable: function () |
michael@0 | 1794 | { |
michael@0 | 1795 | // Simplify the representation if we don't have other details. |
michael@0 | 1796 | if (!this.entityID && !this._unknownProperties) { |
michael@0 | 1797 | return "copy"; |
michael@0 | 1798 | } |
michael@0 | 1799 | |
michael@0 | 1800 | let serializable = { type: "copy", |
michael@0 | 1801 | entityID: this.entityID }; |
michael@0 | 1802 | serializeUnknownProperties(this, serializable); |
michael@0 | 1803 | return serializable; |
michael@0 | 1804 | }, |
michael@0 | 1805 | |
michael@0 | 1806 | /** |
michael@0 | 1807 | * Implements "DownloadSaver.getSha256Hash" |
michael@0 | 1808 | */ |
michael@0 | 1809 | getSha256Hash: function () |
michael@0 | 1810 | { |
michael@0 | 1811 | return this._sha256Hash; |
michael@0 | 1812 | }, |
michael@0 | 1813 | |
michael@0 | 1814 | /* |
michael@0 | 1815 | * Implements DownloadSaver.getSignatureInfo. |
michael@0 | 1816 | */ |
michael@0 | 1817 | getSignatureInfo: function () |
michael@0 | 1818 | { |
michael@0 | 1819 | return this._signatureInfo; |
michael@0 | 1820 | } |
michael@0 | 1821 | }; |
michael@0 | 1822 | |
michael@0 | 1823 | /** |
michael@0 | 1824 | * Creates a new DownloadCopySaver object, with its initial state derived from |
michael@0 | 1825 | * its serializable representation. |
michael@0 | 1826 | * |
michael@0 | 1827 | * @param aSerializable |
michael@0 | 1828 | * Serializable representation of a DownloadCopySaver object. |
michael@0 | 1829 | * |
michael@0 | 1830 | * @return The newly created DownloadCopySaver object. |
michael@0 | 1831 | */ |
michael@0 | 1832 | this.DownloadCopySaver.fromSerializable = function (aSerializable) { |
michael@0 | 1833 | let saver = new DownloadCopySaver(); |
michael@0 | 1834 | if ("entityID" in aSerializable) { |
michael@0 | 1835 | saver.entityID = aSerializable.entityID; |
michael@0 | 1836 | } |
michael@0 | 1837 | |
michael@0 | 1838 | deserializeUnknownProperties(saver, aSerializable, property => |
michael@0 | 1839 | property != "entityID" && property != "type"); |
michael@0 | 1840 | |
michael@0 | 1841 | return saver; |
michael@0 | 1842 | }; |
michael@0 | 1843 | |
michael@0 | 1844 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 1845 | //// DownloadLegacySaver |
michael@0 | 1846 | |
michael@0 | 1847 | /** |
michael@0 | 1848 | * Saver object that integrates with the legacy nsITransfer interface. |
michael@0 | 1849 | * |
michael@0 | 1850 | * For more background on the process, see the DownloadLegacyTransfer object. |
michael@0 | 1851 | */ |
michael@0 | 1852 | this.DownloadLegacySaver = function() |
michael@0 | 1853 | { |
michael@0 | 1854 | this.deferExecuted = Promise.defer(); |
michael@0 | 1855 | this.deferCanceled = Promise.defer(); |
michael@0 | 1856 | } |
michael@0 | 1857 | |
michael@0 | 1858 | this.DownloadLegacySaver.prototype = { |
michael@0 | 1859 | __proto__: DownloadSaver.prototype, |
michael@0 | 1860 | |
michael@0 | 1861 | /** |
michael@0 | 1862 | * Save the SHA-256 hash in raw bytes of the downloaded file. This may be |
michael@0 | 1863 | * null when nsExternalHelperAppService (and thus BackgroundFileSaver) is not |
michael@0 | 1864 | * invoked. |
michael@0 | 1865 | */ |
michael@0 | 1866 | _sha256Hash: null, |
michael@0 | 1867 | |
michael@0 | 1868 | /** |
michael@0 | 1869 | * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert |
michael@0 | 1870 | * if the file is signed. This is empty if the file is unsigned, and null |
michael@0 | 1871 | * unless BackgroundFileSaver has successfully completed saving the file. |
michael@0 | 1872 | */ |
michael@0 | 1873 | _signatureInfo: null, |
michael@0 | 1874 | |
michael@0 | 1875 | /** |
michael@0 | 1876 | * nsIRequest object associated to the status and progress updates we |
michael@0 | 1877 | * received. This object is null before we receive the first status and |
michael@0 | 1878 | * progress update, and is also reset to null when the download is stopped. |
michael@0 | 1879 | */ |
michael@0 | 1880 | request: null, |
michael@0 | 1881 | |
michael@0 | 1882 | /** |
michael@0 | 1883 | * This deferred object contains a promise that is resolved as soon as this |
michael@0 | 1884 | * download finishes successfully, and is rejected in case the download is |
michael@0 | 1885 | * canceled or receives a failure notification through nsITransfer. |
michael@0 | 1886 | */ |
michael@0 | 1887 | deferExecuted: null, |
michael@0 | 1888 | |
michael@0 | 1889 | /** |
michael@0 | 1890 | * This deferred object contains a promise that is resolved if the download |
michael@0 | 1891 | * receives a cancellation request through the "cancel" method, and is never |
michael@0 | 1892 | * rejected. The nsITransfer implementation will register a handler that |
michael@0 | 1893 | * actually causes the download cancellation. |
michael@0 | 1894 | */ |
michael@0 | 1895 | deferCanceled: null, |
michael@0 | 1896 | |
michael@0 | 1897 | /** |
michael@0 | 1898 | * This is populated with the value of the aSetProgressBytesFn argument of the |
michael@0 | 1899 | * "execute" method, and is null before the method is called. |
michael@0 | 1900 | */ |
michael@0 | 1901 | setProgressBytesFn: null, |
michael@0 | 1902 | |
michael@0 | 1903 | /** |
michael@0 | 1904 | * Called by the nsITransfer implementation while the download progresses. |
michael@0 | 1905 | * |
michael@0 | 1906 | * @param aCurrentBytes |
michael@0 | 1907 | * Number of bytes transferred until now. |
michael@0 | 1908 | * @param aTotalBytes |
michael@0 | 1909 | * Total number of bytes to be transferred, or -1 if unknown. |
michael@0 | 1910 | */ |
michael@0 | 1911 | onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes) |
michael@0 | 1912 | { |
michael@0 | 1913 | // Ignore progress notifications until we are ready to process them. |
michael@0 | 1914 | if (!this.setProgressBytesFn) { |
michael@0 | 1915 | return; |
michael@0 | 1916 | } |
michael@0 | 1917 | |
michael@0 | 1918 | let hasPartFile = !!this.download.target.partFilePath; |
michael@0 | 1919 | |
michael@0 | 1920 | this.progressWasNotified = true; |
michael@0 | 1921 | this.setProgressBytesFn(aCurrentBytes, aTotalBytes, |
michael@0 | 1922 | aCurrentBytes > 0 && hasPartFile); |
michael@0 | 1923 | }, |
michael@0 | 1924 | |
michael@0 | 1925 | /** |
michael@0 | 1926 | * Whether the onProgressBytes function has been called at least once. |
michael@0 | 1927 | */ |
michael@0 | 1928 | progressWasNotified: false, |
michael@0 | 1929 | |
michael@0 | 1930 | /** |
michael@0 | 1931 | * Called by the nsITransfer implementation when the request has started. |
michael@0 | 1932 | * |
michael@0 | 1933 | * @param aRequest |
michael@0 | 1934 | * nsIRequest associated to the status update. |
michael@0 | 1935 | * @param aAlreadyAddedToHistory |
michael@0 | 1936 | * Indicates that the nsIExternalHelperAppService component already |
michael@0 | 1937 | * added the download to the browsing history, unless it was started |
michael@0 | 1938 | * from a private browsing window. When this parameter is false, the |
michael@0 | 1939 | * download is added to the browsing history here. Private downloads |
michael@0 | 1940 | * are never added to history even if this parameter is false. |
michael@0 | 1941 | */ |
michael@0 | 1942 | onTransferStarted: function (aRequest, aAlreadyAddedToHistory) |
michael@0 | 1943 | { |
michael@0 | 1944 | // Store the entity ID to use for resuming if required. |
michael@0 | 1945 | if (this.download.tryToKeepPartialData && |
michael@0 | 1946 | aRequest instanceof Ci.nsIResumableChannel) { |
michael@0 | 1947 | try { |
michael@0 | 1948 | // If reading the ID succeeds, the source is resumable. |
michael@0 | 1949 | this.entityID = aRequest.entityID; |
michael@0 | 1950 | } catch (ex if ex instanceof Components.Exception && |
michael@0 | 1951 | ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { } |
michael@0 | 1952 | } |
michael@0 | 1953 | |
michael@0 | 1954 | // For legacy downloads, we must update the referrer at this time. |
michael@0 | 1955 | if (aRequest instanceof Ci.nsIHttpChannel && aRequest.referrer) { |
michael@0 | 1956 | this.download.source.referrer = aRequest.referrer.spec; |
michael@0 | 1957 | } |
michael@0 | 1958 | |
michael@0 | 1959 | if (!aAlreadyAddedToHistory) { |
michael@0 | 1960 | this.addToHistory(); |
michael@0 | 1961 | } |
michael@0 | 1962 | }, |
michael@0 | 1963 | |
michael@0 | 1964 | /** |
michael@0 | 1965 | * Called by the nsITransfer implementation when the request has finished. |
michael@0 | 1966 | * |
michael@0 | 1967 | * @param aRequest |
michael@0 | 1968 | * nsIRequest associated to the status update. |
michael@0 | 1969 | * @param aStatus |
michael@0 | 1970 | * Status code received by the nsITransfer implementation. |
michael@0 | 1971 | */ |
michael@0 | 1972 | onTransferFinished: function DLS_onTransferFinished(aRequest, aStatus) |
michael@0 | 1973 | { |
michael@0 | 1974 | // Store a reference to the request, used when handling completion. |
michael@0 | 1975 | this.request = aRequest; |
michael@0 | 1976 | |
michael@0 | 1977 | if (Components.isSuccessCode(aStatus)) { |
michael@0 | 1978 | this.deferExecuted.resolve(); |
michael@0 | 1979 | } else { |
michael@0 | 1980 | // Infer the origin of the error from the failure code, because more |
michael@0 | 1981 | // specific data is not available through the nsITransfer implementation. |
michael@0 | 1982 | let properties = { result: aStatus, inferCause: true }; |
michael@0 | 1983 | this.deferExecuted.reject(new DownloadError(properties)); |
michael@0 | 1984 | } |
michael@0 | 1985 | }, |
michael@0 | 1986 | |
michael@0 | 1987 | /** |
michael@0 | 1988 | * When the first execution of the download finished, it can be restarted by |
michael@0 | 1989 | * using a DownloadCopySaver object instead of the original legacy component |
michael@0 | 1990 | * that executed the download. |
michael@0 | 1991 | */ |
michael@0 | 1992 | firstExecutionFinished: false, |
michael@0 | 1993 | |
michael@0 | 1994 | /** |
michael@0 | 1995 | * In case the download is restarted after the first execution finished, this |
michael@0 | 1996 | * property contains a reference to the DownloadCopySaver that is executing |
michael@0 | 1997 | * the new download attempt. |
michael@0 | 1998 | */ |
michael@0 | 1999 | copySaver: null, |
michael@0 | 2000 | |
michael@0 | 2001 | /** |
michael@0 | 2002 | * String corresponding to the entityID property of the nsIResumableChannel |
michael@0 | 2003 | * used to execute the download, or null if the channel was not resumable or |
michael@0 | 2004 | * the saver was instructed not to keep partially downloaded data. |
michael@0 | 2005 | */ |
michael@0 | 2006 | entityID: null, |
michael@0 | 2007 | |
michael@0 | 2008 | /** |
michael@0 | 2009 | * Implements "DownloadSaver.execute". |
michael@0 | 2010 | */ |
michael@0 | 2011 | execute: function DLS_execute(aSetProgressBytesFn) |
michael@0 | 2012 | { |
michael@0 | 2013 | // Check if this is not the first execution of the download. The Download |
michael@0 | 2014 | // object guarantees that this function is not re-entered during execution. |
michael@0 | 2015 | if (this.firstExecutionFinished) { |
michael@0 | 2016 | if (!this.copySaver) { |
michael@0 | 2017 | this.copySaver = new DownloadCopySaver(); |
michael@0 | 2018 | this.copySaver.download = this.download; |
michael@0 | 2019 | this.copySaver.entityID = this.entityID; |
michael@0 | 2020 | this.copySaver.alreadyAddedToHistory = true; |
michael@0 | 2021 | } |
michael@0 | 2022 | return this.copySaver.execute.apply(this.copySaver, arguments); |
michael@0 | 2023 | } |
michael@0 | 2024 | |
michael@0 | 2025 | this.setProgressBytesFn = aSetProgressBytesFn; |
michael@0 | 2026 | |
michael@0 | 2027 | return Task.spawn(function task_DLS_execute() { |
michael@0 | 2028 | try { |
michael@0 | 2029 | // Wait for the component that executes the download to finish. |
michael@0 | 2030 | yield this.deferExecuted.promise; |
michael@0 | 2031 | |
michael@0 | 2032 | // At this point, the "request" property has been populated. Ensure we |
michael@0 | 2033 | // report the value of "Content-Length", if available, even if the |
michael@0 | 2034 | // download didn't generate any progress events. |
michael@0 | 2035 | if (!this.progressWasNotified && |
michael@0 | 2036 | this.request instanceof Ci.nsIChannel && |
michael@0 | 2037 | this.request.contentLength >= 0) { |
michael@0 | 2038 | aSetProgressBytesFn(0, this.request.contentLength); |
michael@0 | 2039 | } |
michael@0 | 2040 | |
michael@0 | 2041 | // If the component executing the download provides the path of a |
michael@0 | 2042 | // ".part" file, it means that it expects the listener to move the file |
michael@0 | 2043 | // to its final target path when the download succeeds. In this case, |
michael@0 | 2044 | // an empty ".part" file is created even if no data was received from |
michael@0 | 2045 | // the source. |
michael@0 | 2046 | if (this.download.target.partFilePath) { |
michael@0 | 2047 | yield OS.File.move(this.download.target.partFilePath, |
michael@0 | 2048 | this.download.target.path); |
michael@0 | 2049 | } else { |
michael@0 | 2050 | // The download implementation may not have created the target file if |
michael@0 | 2051 | // no data was received from the source. In this case, ensure that an |
michael@0 | 2052 | // empty file is created as expected. |
michael@0 | 2053 | try { |
michael@0 | 2054 | // This atomic operation is more efficient than an existence check. |
michael@0 | 2055 | let file = yield OS.File.open(this.download.target.path, |
michael@0 | 2056 | { create: true }); |
michael@0 | 2057 | yield file.close(); |
michael@0 | 2058 | } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { } |
michael@0 | 2059 | } |
michael@0 | 2060 | } catch (ex) { |
michael@0 | 2061 | // Ensure we always remove the final target file on failure, |
michael@0 | 2062 | // independently of which code path failed. In some cases, the |
michael@0 | 2063 | // component executing the download may have already removed the file. |
michael@0 | 2064 | try { |
michael@0 | 2065 | yield OS.File.remove(this.download.target.path); |
michael@0 | 2066 | } catch (e2) { |
michael@0 | 2067 | // If we failed during the operation, we report the error but use the |
michael@0 | 2068 | // original one as the failure reason of the download. Note that on |
michael@0 | 2069 | // Windows we may get an access denied error instead of a no such file |
michael@0 | 2070 | // error if the file existed before, and was recently deleted. |
michael@0 | 2071 | if (!(e2 instanceof OS.File.Error && |
michael@0 | 2072 | (e2.becauseNoSuchFile || e2.becauseAccessDenied))) { |
michael@0 | 2073 | Cu.reportError(e2); |
michael@0 | 2074 | } |
michael@0 | 2075 | } |
michael@0 | 2076 | // In case the operation failed, ensure we stop downloading data. Since |
michael@0 | 2077 | // we never re-enter this function, deferCanceled is always available. |
michael@0 | 2078 | this.deferCanceled.resolve(); |
michael@0 | 2079 | throw ex; |
michael@0 | 2080 | } finally { |
michael@0 | 2081 | // We don't need the reference to the request anymore. We must also set |
michael@0 | 2082 | // deferCanceled to null in order to free any indirect references it |
michael@0 | 2083 | // may hold to the request. |
michael@0 | 2084 | this.request = null; |
michael@0 | 2085 | this.deferCanceled = null; |
michael@0 | 2086 | // Allow the download to restart through a DownloadCopySaver. |
michael@0 | 2087 | this.firstExecutionFinished = true; |
michael@0 | 2088 | } |
michael@0 | 2089 | }.bind(this)); |
michael@0 | 2090 | }, |
michael@0 | 2091 | |
michael@0 | 2092 | /** |
michael@0 | 2093 | * Implements "DownloadSaver.cancel". |
michael@0 | 2094 | */ |
michael@0 | 2095 | cancel: function DLS_cancel() |
michael@0 | 2096 | { |
michael@0 | 2097 | // We may be using a DownloadCopySaver to handle resuming. |
michael@0 | 2098 | if (this.copySaver) { |
michael@0 | 2099 | return this.copySaver.cancel.apply(this.copySaver, arguments); |
michael@0 | 2100 | } |
michael@0 | 2101 | |
michael@0 | 2102 | // If the download hasn't stopped already, resolve deferCanceled so that the |
michael@0 | 2103 | // operation is canceled as soon as a cancellation handler is registered. |
michael@0 | 2104 | // Note that the handler might not have been registered yet. |
michael@0 | 2105 | if (this.deferCanceled) { |
michael@0 | 2106 | this.deferCanceled.resolve(); |
michael@0 | 2107 | } |
michael@0 | 2108 | }, |
michael@0 | 2109 | |
michael@0 | 2110 | /** |
michael@0 | 2111 | * Implements "DownloadSaver.removePartialData". |
michael@0 | 2112 | */ |
michael@0 | 2113 | removePartialData: function () |
michael@0 | 2114 | { |
michael@0 | 2115 | // DownloadCopySaver and DownloadLeagcySaver use the same logic for removing |
michael@0 | 2116 | // partially downloaded data, though this implementation isn't shared by |
michael@0 | 2117 | // other saver types, thus it isn't found on their shared prototype. |
michael@0 | 2118 | return DownloadCopySaver.prototype.removePartialData.call(this); |
michael@0 | 2119 | }, |
michael@0 | 2120 | |
michael@0 | 2121 | /** |
michael@0 | 2122 | * Implements "DownloadSaver.toSerializable". |
michael@0 | 2123 | */ |
michael@0 | 2124 | toSerializable: function () |
michael@0 | 2125 | { |
michael@0 | 2126 | // This object depends on legacy components that are created externally, |
michael@0 | 2127 | // thus it cannot be rebuilt during deserialization. To support resuming |
michael@0 | 2128 | // across different browser sessions, this object is transformed into a |
michael@0 | 2129 | // DownloadCopySaver for the purpose of serialization. |
michael@0 | 2130 | return DownloadCopySaver.prototype.toSerializable.call(this); |
michael@0 | 2131 | }, |
michael@0 | 2132 | |
michael@0 | 2133 | /** |
michael@0 | 2134 | * Implements "DownloadSaver.getSha256Hash". |
michael@0 | 2135 | */ |
michael@0 | 2136 | getSha256Hash: function () |
michael@0 | 2137 | { |
michael@0 | 2138 | if (this.copySaver) { |
michael@0 | 2139 | return this.copySaver.getSha256Hash(); |
michael@0 | 2140 | } |
michael@0 | 2141 | return this._sha256Hash; |
michael@0 | 2142 | }, |
michael@0 | 2143 | |
michael@0 | 2144 | /** |
michael@0 | 2145 | * Called by the nsITransfer implementation when the hash is available. |
michael@0 | 2146 | */ |
michael@0 | 2147 | setSha256Hash: function (hash) |
michael@0 | 2148 | { |
michael@0 | 2149 | this._sha256Hash = hash; |
michael@0 | 2150 | }, |
michael@0 | 2151 | |
michael@0 | 2152 | /** |
michael@0 | 2153 | * Implements "DownloadSaver.getSignatureInfo". |
michael@0 | 2154 | */ |
michael@0 | 2155 | getSignatureInfo: function () |
michael@0 | 2156 | { |
michael@0 | 2157 | if (this.copySaver) { |
michael@0 | 2158 | return this.copySaver.getSignatureInfo(); |
michael@0 | 2159 | } |
michael@0 | 2160 | return this._signatureInfo; |
michael@0 | 2161 | }, |
michael@0 | 2162 | |
michael@0 | 2163 | /** |
michael@0 | 2164 | * Called by the nsITransfer implementation when the hash is available. |
michael@0 | 2165 | */ |
michael@0 | 2166 | setSignatureInfo: function (signatureInfo) |
michael@0 | 2167 | { |
michael@0 | 2168 | this._signatureInfo = signatureInfo; |
michael@0 | 2169 | }, |
michael@0 | 2170 | }; |
michael@0 | 2171 | |
michael@0 | 2172 | /** |
michael@0 | 2173 | * Returns a new DownloadLegacySaver object. This saver type has a |
michael@0 | 2174 | * deserializable form only when creating a new object in memory, because it |
michael@0 | 2175 | * cannot be serialized to disk. |
michael@0 | 2176 | */ |
michael@0 | 2177 | this.DownloadLegacySaver.fromSerializable = function () { |
michael@0 | 2178 | return new DownloadLegacySaver(); |
michael@0 | 2179 | }; |