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