Debian package: Declare compliance with Debian Policy 4.3.0
[conkeror.git] / modules / download-manager.js
blobf66d736ea6ca211e0fbb4bb827789b70bd0bd420
1 /**
2  * (C) Copyright 2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2009 John Foerch
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
9 require("special-buffer.js");
10 require("mime-type-override.js");
11 require("minibuffer-read-mime-type.js");
12 require("compat/Map.js");
14 var download_manager_service = Cc["@mozilla.org/download-manager;1"]
15     .getService(Ci.nsIDownloadManager);
17 var unmanaged_download_info_list = [];
19 var id_to_download_info = new Map();
21 try {
22     Cu.import("resource://gre/modules/Downloads.jsm");
23     if (typeof(Downloads.getList) == 'undefined')
24         throw "bad Downloads.jsm version";
25     var use_downloads_jsm = true;
27     var lookup_download = function lookup_download(download) {
28         return id_to_download_info.get(download);
29     }
30 } catch (e) {
31     var use_downloads_jsm = false;
33     var lookup_download = function lookup_download(download) {
34         return id_to_download_info.get(download.id);
35     }
39 // Import these constants for convenience
40 const DOWNLOAD_NOTSTARTED = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
41 const DOWNLOAD_DOWNLOADING = Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING;
42 const DOWNLOAD_FINISHED = Ci.nsIDownloadManager.DOWNLOAD_FINISHED;
43 const DOWNLOAD_FAILED = Ci.nsIDownloadManager.DOWNLOAD_FAILED;
44 const DOWNLOAD_CANCELED = Ci.nsIDownloadManager.DOWNLOAD_CANCELED;
45 const DOWNLOAD_PAUSED = Ci.nsIDownloadManager.DOWNLOAD_PAUSED;
46 const DOWNLOAD_QUEUED = Ci.nsIDownloadManager.DOWNLOAD_QUEUED;
47 const DOWNLOAD_SCANNING = Ci.nsIDownloadManager.DOWNLOAD_SCANNING;
50 const DOWNLOAD_NOT_TEMPORARY = 0;
51 const DOWNLOAD_TEMPORARY_FOR_ACTION = 1;
52 const DOWNLOAD_TEMPORARY_FOR_COMMAND = 2;
54 function download_info (source_buffer, mozilla_info, target_file) {
55     this.source_buffer = source_buffer;
56     this.target_file = target_file;
57     if (mozilla_info != null)
58         this.attach(mozilla_info);
60 download_info.prototype = {
61     constructor: download_info,
62     target_file: null,
63     shell_command: null,
64     shell_command_cwd: null,
65     temporary_status: DOWNLOAD_NOT_TEMPORARY,
66     action_description: null,
67     set_shell_command: function (str, cwd) {
68         this.shell_command = str;
69         this.shell_command_cwd = cwd;
70         if (this.mozilla_info)
71             download_shell_command_change_hook.run(this);
72     },
74     /**
75      * None of the following members may be used until attach is called
76      */
78     // Reflectors to properties of nsIDownload
79     get source () { return this.mozilla_info.source; },
80     get id () { return this.mozilla_info.id; },
81     get referrer () { return this.mozilla_info.referrer; },
83     target_file_text: function () {
84         let target = this.target_file.path;
85         let display = this.display_name;
86         if (target.indexOf(display, target.length - display.length) == -1)
87             target += " (" + display + ")";
88         return target;
89     },
91     throw_if_removed: function () {
92         if (this.removed)
93             throw interactive_error("Download has already been removed from the download manager.");
94     },
96     throw_state_error: function () {
97         switch (this.state) {
98         case DOWNLOAD_DOWNLOADING:
99             throw interactive_error("Download is already in progress.");
100         case DOWNLOAD_FINISHED:
101             throw interactive_error("Download has already completed.");
102         case DOWNLOAD_FAILED:
103             throw interactive_error("Download has already failed.");
104         case DOWNLOAD_CANCELED:
105             throw interactive_error("Download has already been canceled.");
106         case DOWNLOAD_PAUSED:
107             throw interactive_error("Download has already been paused.");
108         case DOWNLOAD_QUEUED:
109             throw interactive_error("Download is queued.");
110         default:
111             throw new Error("Download has unexpected state: " + this.state);
112         }
113     },
115     // Download manager operations
116     cancel: function () {
117         this.throw_if_removed();
118         switch (this.state) {
119         case DOWNLOAD_DOWNLOADING:
120         case DOWNLOAD_PAUSED:
121         case DOWNLOAD_QUEUED:
122             if (use_downloads_jsm) {
123                 yield this.mozilla_info.finalize(true);
124             } else {
125                 try {
126                     download_manager_service.cancelDownload(this.id);
127                 } catch (e) {
128                     throw interactive_error("Download cannot be canceled.");
129                 }
130             }
131             break;
132         default:
133             this.throw_state_error();
134         }
135     },
137     retry: function () {
138         this.throw_if_removed();
139         switch (this.state) {
140         case DOWNLOAD_CANCELED:
141         case DOWNLOAD_FAILED:
142             if (use_downloads_jsm) {
143                 yield this.mozilla_info.start();
144             } else {
145                 try {
146                     download_manager_service.retryDownload(this.id);
147                 } catch (e) {
148                     throw interactive_error("Download cannot be retried.");
149                 }
150             }
151             break;
152         default:
153             this.throw_state_error();
154         }
155     },
157     resume: function () {
158         this.throw_if_removed();
159         switch (this.state) {
160         case DOWNLOAD_PAUSED:
161             if (use_downloads_jsm) {
162                 yield this.mozilla_info.start();
163             } else {
165                 try {
166                     download_manager_service.resumeDownload(this.id);
167                 } catch (e) {
168                     throw interactive_error("Download cannot be resumed.");
169                 }
170             }
171             break;
172         default:
173             this.throw_state_error();
174         }
175     },
177     pause: function () {
178         this.throw_if_removed();
179         switch (this.state) {
180         case DOWNLOAD_DOWNLOADING:
181         case DOWNLOAD_QUEUED:
182             if (use_downloads_jsm) {
183                 yield this.mozilla_info.cancel();
184             } else {
185                 try {
186                     download_manager_service.pauseDownload(this.id);
187                 } catch (e) {
188                     throw interactive_error("Download cannot be paused.");
189                 }
190             }
191             break;
192         default:
193             this.throw_state_error();
194         }
195     },
197     remove: function () {
198         this.throw_if_removed();
199         switch (this.state) {
200         case DOWNLOAD_FAILED:
201         case DOWNLOAD_CANCELED:
202         case DOWNLOAD_FINISHED:
203             if (use_downloads_jsm) {
204                 let list = yield Downloads.getList(Downloads.ALL);
205                 yield list.remove(this.mozilla_info);
206             } else {
207                 try {
208                     download_manager_service.removeDownload(this.id);
209                 } catch (e) {
210                     throw interactive_error("Download cannot be removed.");
211                 }
212             }
213                 break;
214         default:
215             throw interactive_error("Download is still in progress.");
216         }
217     },
219     delete_target: function () {
220         if (this.state != DOWNLOAD_FINISHED)
221             throw interactive_error("Download has not finished.");
222         try {
223             this.target_file.remove(false);
224         } catch (e) {
225             if ("result" in e) {
226                 switch (e.result) {
227                 case Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
228                     throw interactive_error("File has already been deleted.");
229                 case Cr.NS_ERROR_FILE_ACCESS_DENIED:
230                     throw interactive_error("Access denied");
231                 case Cr.NS_ERROR_FILE_DIR_NOT_EMPTY:
232                     throw interactive_error("Failed to delete file.");
233                 }
234             }
235             throw e;
236         }
237     }
240 if (!use_downloads_jsm) {
241     download_info.prototype.__proto__ = {
242         attach: function (mozilla_info, existing) {
243             if (!this.target_file)
244                 this.__defineGetter__("target_file", function () {
245                     return this.mozilla_info.targetFile;
246                 });
247             else if (this.target_file.path != mozilla_info.targetFile.path)
248                 throw interactive_error("Download target file unexpected.");
250             this.mozilla_info = mozilla_info;
252             if (use_downloads_jsm) {
253                 id_to_download_info.set(mozilla_info, this);
254             } else {
255                 id_to_download_info.set(mozilla_info.id, this);
256             }
258             if (existing)
259                 existing_download_added_hook.run(this);
260             else
261                 download_added_hook.run(this);
262         },
263         get source_uri_string () { return this.mozilla_info.source.spec; },
264         get source_uri () { return this.mozilla_info.source; },
265         get display_name () { return this.mozilla_info.displayName; },
266         get amount_transferred () { return this.mozilla_info.amountTransferred; },
267         get percent_complete () { return this.mozilla_info.percentComplete; },
268         get speed () { return this.mozilla_info.speed; },
269         get state () { return this.mozilla_info.state; },
270         get start_time () { return this.mozilla_info.startTime / 1000; },
271         get MIME_info () { return this.mozilla_info.MIMEInfo; },
272         get MIME_type () {
273             if (this.MIME_info)
274                 return this.MIME_info.MIMEType;
275             return null;
276         },
277         get size () {
278             var s = this.mozilla_info.size;
279             /* nsIDownload.size is a PRUint64, and will have value
280              * LL_MAXUINT (2^64 - 1) to indicate an unknown size.  Because
281              * JavaScript only has a double numerical type, this value
282              * cannot be represented exactly, so 2^36 is used instead as the cutoff. */
283             if (s < 68719476736 /* 2^36 */)
284                 return s;
285             return -1;
286         },
287     };
288 } else {
289     download_info.prototype.__proto__ = {
290         attach: function (mozilla_info, existing) {
291             if (!this.target_file)
292                 this.__defineGetter__("target_file", function () {
293                     return make_file(this.mozilla_info.target.path);
294                 });
295             else if (this.target_file.path != mozilla_info.target.path)
296                 throw interactive_error("Download target file unexpected.");
298             this.mozilla_info = mozilla_info;
299             id_to_download_info.set(mozilla_info, this);
301             if (existing)
302                 existing_download_added_hook.run(this);
303             else
304                 download_added_hook.run(this);
305         },
306         get source_uri () { return make_uri(this.mozilla_info.source.url); },
307         get source_uri_string () { return this.mozilla_info.source.url; },
308         get display_name () { return this.mozilla_info.target.path; },
309         get amount_transferred () { return this.mozilla_info.currentBytes; },
310         get percent_complete () { return this.mozilla_info.progress; },
311         get speed () { return 1000 * this.amount_transferred / (Date.now() - this.start_time); },
312         get start_time () { return this.mozilla_info.startTime.getTime(); },
313         get MIME_type () { return this.mozilla_info.contentType; },
314         get state () {
315             if (this.mozilla_info.succeeded)
316                 return DOWNLOAD_FINISHED;
317             if (this.mozilla_info.canceled)
318                 return DOWNLOAD_CANCELED;
319             if (this.mozilla_info.error)
320                 return DOWNLOAD_FAILED;
321             if (!this.mozilla_info.startTime)
322                 return DOWNLOAD_NOTSTARTED;
323             return DOWNLOAD_DOWNLOADING;
324         },
325         get size () {
326             if (this.mozilla_info.hasProgress)
327                 return this.mozilla_info.totalBytes;
328             return -1;
329         },
330     };
333 var define_download_local_hook = simple_local_hook_definer();
335 function register_download (buffer, source_uri, target_file) {
336     var info = new download_info(buffer, null, target_file);
337     info.registered_time_stamp = Date.now();
338     info.registered_source_uri = source_uri;
339     unmanaged_download_info_list.push(info);
340     return info;
343 function match_registered_download (source) {
344     let list = unmanaged_download_info_list;
345     let t = Date.now();
346     for (let i = 0; i < list.length; ++i) {
347         let x = list[i];
348         if (x.registered_source_uri.spec == source) {
349             list.splice(i, 1);
350             return x;
351         }
352         if (t - x.registered_time_stamp > download_info_max_queue_delay) {
353             list.splice(i, 1);
354             --i;
355             continue;
356         }
357     }
358     return null;
361 define_download_local_hook("existing_download_added_hook");
362 define_download_local_hook("download_added_hook");
363 define_download_local_hook("download_removed_hook");
364 define_download_local_hook("download_finished_hook");
365 define_download_local_hook("download_progress_change_hook");
366 define_download_local_hook("download_state_change_hook");
367 define_download_local_hook("download_shell_command_change_hook");
369 define_variable('delete_temporary_files_for_command', true,
370     'If this is set to true, temporary files downloaded to run a command '+
371     'on them will be deleted once the command completes. If not, the file '+
372     'will stay around forever unless deleted outside the browser.');
374 var download_info_max_queue_delay = 100;
376 if (!use_downloads_jsm) {
378     var download_progress_listener = {
379         QueryInterface: generate_QI(Ci.nsIDownloadProgressListener),
381         onDownloadStateChange: function (state, download) {
382             var info = null;
383             /* FIXME: Determine if only new downloads will have this state
384              * as their previous state. */
386             dumpln("download state change: " + download.source.spec + ": " + state + ", " + download.state + ", " + download.id);
388             if (state == DOWNLOAD_NOTSTARTED) {
389                 info = match_registered_download(download.source.spec);
390                 if (info == null) {
391                     info = new download_info(null, download);
392                     dumpln("error: encountered unknown new download");
393                 } else {
394                     info.attach(download);
395                 }
396             } else {
397                 info = id_to_download_info.get(download.id);
398                 if (info == null) {
399                     dumpln("Error: encountered unknown download");
401                 } else {
402                     info.mozilla_info = download;
403                     download_state_change_hook.run(info);
404                     if (info.state == DOWNLOAD_FINISHED) {
405                         download_finished_hook.run(info);
407                         if (info.shell_command != null) {
408                             info.running_shell_command = true;
409                             spawn(function () {
410                                 try {
411                                     yield shell_command_with_argument(info.shell_command,
412                                                                       info.target_file.path,
413                                                                       $cwd = info.shell_command_cwd);
414                                 } catch (e) {
415                                     handle_interactive_error(info.source_buffer.window, e);
416                                 } finally  {
417                                     if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
418                                         if(delete_temporary_files_for_command) {
419                                             info.target_file.remove(false /* not recursive */);
420                                         }
421                                     info.running_shell_command = false;
422                                     download_shell_command_change_hook.run(info);
423                                 }
424                             }());
425                             download_shell_command_change_hook.run(info);
426                         }
427                     }
428                 }
429             }
430         },
432         onProgressChange: function (progress, request, cur_self_progress, max_self_progress,
433                                     cur_total_progress, max_total_progress,
434                                     download) {
435             var info = id_to_download_info.get(download.id);
436             if (info == null) {
437                 dumpln("error: encountered unknown download in progress change");
438                 return;
439             }
440             info.mozilla_info = download;
441             download_progress_change_hook.run(info);
442             //dumpln("download progress change: " + download.source.spec + ": " + cur_self_progress + "/" + max_self_progress + " "
443             // + cur_total_progress + "/" + max_total_progress + ", " + download.state + ", " + download.id);
444         },
446         onSecurityChange: function (progress, request, state, download) {
447         },
449         onStateChange: function (progress, request, state_flags, status, download) {
450         }
451     };
453     var download_observer = {
454         observe: function (subject, topic, data) {
455             switch(topic) {
456             case "download-manager-remove-download":
457                 var ids = [];
458                 if (!subject) {
459                     // Remove all downloads
460                     for (let i in id_to_download_info)
461                         ids.push(i);
462                 } else {
463                     let id = subject.QueryInterface(Ci.nsISupportsPRUint32);
464                     /* FIXME: determine if this should really be an error */
465                     if (!(id in id_to_download_info)) {
466                         dumpln("Error: download-manager-remove-download event received for unknown download: " + id);
467                     } else
468                         ids.push(id);
469                 }
470                 for each (let i in ids) {
471                     dumpln("deleting download: " + i);
472                     let d = id_to_download_info[i];
473                     d.removed = true;
474                     download_removed_hook.run(d);
475                     id_to_download_info.delete(i);
476                 }
477                 break;
478             }
479         }
480     };
482     observer_service.addObserver(download_observer, "download-manager-remove-download", false);
484     try {
485         download_manager_service.addListener(download_progress_listener);
486     } catch (e) {
487         dumpln("Failed to register download progress listener.");
488         dump_error(e);
489     }
490 } else {
492     spawn(function() {
493         let list = yield Downloads.getList(Downloads.ALL);
494         let view = {
495             onDownloadAdded: function (download) {
496                 // We never want the automatic launching to be used
497                 // This is set by default when using nsIWebBrowserPersist
498                 download.launchWhenSucceeded = false;
500                 let info = match_registered_download(download.source.url);
502                 if (info == null) {
503                     info = new download_info(null, download);
504                     dumpln("Encountered unknown new download");
505                 } else {
506                     info.attach(download);
507                 }
508             },
509             onDownloadChanged: function (download) {
510                 let info = lookup_download(download);
511                 if (!info)
512                     dumpln("error: onDownloadChanged: encountered unknown download");
513                 else {
514                     download_progress_change_hook.run(info);
515                     download_state_change_hook.run(info);
517                     if (info.state == DOWNLOAD_FINISHED) {
518                         download_finished_hook.run(info);
520                         if (info.shell_command != null) {
521                             info.running_shell_command = true;
522                             spawn(function () {
523                                 try {
524                                     yield shell_command_with_argument(info.shell_command,
525                                                                       info.target_file.path,
526                                                                       $cwd = info.shell_command_cwd);
527                                 } catch (e) {
528                                     handle_interactive_error(info.source_buffer.window, e);
529                                 } finally  {
530                                     if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
531                                         if(delete_temporary_files_for_command) {
532                                             info.target_file.remove(false /* not recursive */);
533                                         }
534                                     info.running_shell_command = false;
535                                     download_shell_command_change_hook.run(info);
536                                 }
537                             }());
538                             download_shell_command_change_hook.run(info);
539                         }
540                     }
541                 }
542             },
543             onDownloadRemoved: function (download) {
544                 let info = lookup_download(download);
545                 if (!info)
546                     dumpln("error: onDownloadRemoved: encountered unknown download");
547                 else {
548                     dumpln("Removing download: " + info.source_uri_string);
549                     info.removed = true;
550                     download_removed_hook.run(info);
551                     id_to_download_info.delete(download);
552                 }
553             }
554         };
555         list.addView(view);
556     }());
559 define_variable("download_buffer_min_update_interval", 2000,
560     "Minimum interval (in milliseconds) between updates in download progress buffers.\n" +
561     "Lowering this interval will increase the promptness of the progress display at " +
562     "the cost of using additional processor time.");
564 function download_buffer_modality (buffer, element) {
565     buffer.keymaps.push(download_buffer_keymap);
568 define_keywords("$info");
569 function download_buffer (window) {
570     this.constructor_begin();
571     keywords(arguments);
572     special_buffer.call(this, window, forward_keywords(arguments));
573     this.info = arguments.$info;
574     this.local.cwd = this.info.target_file.parent;
575     this.description = this.info.source_uri_string;
576     this.update_title();
578     this.progress_change_handler_fn = method_caller(this, this.handle_progress_change);
580     // With Downloads.jsm integration, download_progress_change_hook is redundant with download_state_change_hook
581     if (!use_downloads_jsm)
582         add_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
584     add_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
585     this.command_change_handler_fn = method_caller(this, this.update_command_field);
586     add_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
587     this.modalities.push(download_buffer_modality);
588     this.constructor_end();
590 download_buffer.prototype = {
591     constructor: download_buffer,
592     __proto__: special_buffer.prototype,
593     toString: function () "#<download_buffer>",
595     destroy: function () {
596         if (!use_downloads_jsm)
597             remove_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
599         remove_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
600         remove_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
602         // Remove all node references
603         delete this.status_textnode;
604         delete this.target_file_node;
605         delete this.transferred_div_node;
606         delete this.transferred_textnode;
607         delete this.progress_container_node;
608         delete this.progress_bar_node;
609         delete this.percent_textnode;
610         delete this.time_textnode;
611         delete this.command_div_node;
612         delete this.command_label_textnode;
613         delete this.command_textnode;
615         special_buffer.prototype.destroy.call(this);
616     },
618     update_title: function () {
619         try {
620             // FIXME: do this properly
621             var new_title;
622             var info = this.info;
623             var append_transfer_info = false;
624             var append_speed_info = true;
625             var label = null;
626             switch(info.state) {
627             case DOWNLOAD_DOWNLOADING:
628                 label = "Downloading";
629                 append_transfer_info = true;
630                 break;
631             case DOWNLOAD_FINISHED:
632                 label = "Download complete";
633                 break;
634             case DOWNLOAD_FAILED:
635                 label = "Download failed";
636                 append_transfer_info = true;
637                 append_speed_info = false;
638                 break;
639             case DOWNLOAD_CANCELED:
640                 label = "Download canceled";
641                 append_transfer_info = true;
642                 append_speed_info = false;
643                 break;
644             case DOWNLOAD_PAUSED:
645                 label = "Download paused";
646                 append_transfer_info = true;
647                 append_speed_info = false;
648                 break;
649             case DOWNLOAD_QUEUED:
650             default:
651                 label = "Download queued";
652                 break;
653             }
655             if (append_transfer_info) {
656                 if (append_speed_info)
657                     new_title = label + " at " + pretty_print_file_size(info.speed).join(" ") + "/s: ";
658                 else
659                     new_title = label + ": ";
660                 var trans = pretty_print_file_size(info.amount_transferred);
661                 if (info.size >= 0) {
662                     var total = pretty_print_file_size(info.size);
663                     if (trans[1] == total[1])
664                         new_title += trans[0] + "/" + total[0] + " " + total[1];
665                     else
666                         new_title += trans.join(" ") + "/" + total.join(" ");
667                 } else
668                     new_title += trans.join(" ");
669                 if (info.percent_complete >= 0)
670                     new_title += " (" + info.percent_complete + "%)";
671             } else
672                 new_title = label;
673             if (new_title != this.title) {
674                 this.title = new_title;
675                 return true;
676             }
677             return false;
678         } catch (e) {
679             dump_error(e);
680             throw e;
681         }
682     },
684     handle_progress_change: function () {
685         var cur_time = Date.now();
686         if (this.last_update == null ||
687             (cur_time - this.last_update) > download_buffer_min_update_interval ||
688             this.info.state != this.previous_state) {
690             if (this.update_title())
691                 buffer_title_change_hook.run(this);
693             if (this.generated) {
694                 this.update_fields();
695             }
696             this.previous_status = this.info.status;
697             this.last_update = cur_time;
698         }
699     },
701     generate: function () {
702         var d = this.document;
703         var g = new dom_generator(d, XHTML_NS);
705         /* Warning: If any additional node references are saved in
706          * this function, appropriate code to delete the saved
707          * properties must be added to destroy method. */
709         var info = this.info;
711         d.body.setAttribute("class", "download-buffer");
713         g.add_stylesheet("chrome://conkeror-gui/content/downloads.css");
715         var row, cell;
716         var table = g.element("table", d.body);
718         row = g.element("tr", table, "class", "download-info", "id", "download-source");
719         cell = g.element("td", row, "class", "download-label");
720         this.status_textnode = g.text("", cell);
721         cell = g.element("td", row, "class", "download-value");
722         g.text(info.source_uri_string, cell);
724         row = g.element("tr", table, "class", "download-info", "id", "download-target");
725         cell = g.element("td", row, "class", "download-label");
726         var target_label;
727         if (info.temporary_status != DOWNLOAD_NOT_TEMPORARY)
728             target_label = "Temp. file:";
729         else
730             target_label = "Target:";
731         g.text(target_label, cell);
732         cell = g.element("td", row, "class", "download-value");
733         this.target_file_node = g.text("", cell);
735         row = g.element("tr", table, "class", "download-info", "id", "download-mime-type");
736         cell = g.element("td", row, "class", "download-label");
737         g.text("MIME type:", cell);
738         cell = g.element("td", row, "class", "download-value");
739         g.text(info.MIME_type || "unknown", cell);
741         this.transferred_div_node = row =
742             g.element("tr", table, "class", "download-info", "id", "download-transferred");
743         cell = g.element("td", row, "class", "download-label");
744         g.text("Transferred:", cell);
745         cell = g.element("td", row, "class", "download-value");
746         var sub_item = g.element("div", cell);
747         this.transferred_textnode = g.text("", sub_item);
748         sub_item = g.element("div", cell, "id", "download-percent");
749         this.percent_textnode = g.text("", sub_item);
750         this.progress_container_node = sub_item = g.element("div", cell, "id", "download-progress-container");
751         this.progress_bar_node = g.element("div", sub_item, "id", "download-progress-bar");
753         row = g.element("tr", table, "class", "download-info", "id", "download-time");
754         cell = g.element("td", row, "class", "download-label");
755         g.text("Time:", cell);
756         cell = g.element("td", row, "class", "download-value");
757         this.time_textnode = g.text("", cell);
759         if (info.action_description != null) {
760             row = g.element("tr", table, "class", "download-info", "id", "download-action");
761             cell = g.element("div", row, "class", "download-label");
762             g.text("Action:", cell);
763             cell = g.element("div", row, "class", "download-value");
764             g.text(info.action_description, cell);
765         }
767         this.command_div_node = row = g.element("tr", table, "class", "download-info", "id", "download-command");
768         cell = g.element("td", row, "class", "download-label");
769         this.command_label_textnode = g.text("Run command:", cell);
770         cell = g.element("td", row, "class", "download-value");
771         this.command_textnode = g.text("", cell);
773         this.update_fields();
774         this.update_command_field();
775     },
777     update_fields: function () {
778         if (!this.generated)
779             return;
780         var info = this.info;
781         var label = null;
782         switch (info.state) {
783         case DOWNLOAD_DOWNLOADING:
784             label = "Downloading";
785             break;
786         case DOWNLOAD_FINISHED:
787             label = "Completed";
788             break;
789         case DOWNLOAD_FAILED:
790             label = "Failed";
791             break;
792         case DOWNLOAD_CANCELED:
793             label = "Canceled";
794             break;
795         case DOWNLOAD_PAUSED:
796             label = "Paused";
797             break;
798         case DOWNLOAD_QUEUED:
799         default:
800             label = "Queued";
801             break;
802         }
803         this.status_textnode.nodeValue = label + ":";
804         this.target_file_node.nodeValue = info.target_file_text();
805         this.update_time_field();
807         var tran_text = "";
808         if (info.state == DOWNLOAD_FINISHED)
809             tran_text = pretty_print_file_size(info.size).join(" ");
810         else {
811             var trans = pretty_print_file_size(info.amount_transferred);
812             if (info.size >= 0) {
813                 var total = pretty_print_file_size(info.size);
814                 if (trans[1] == total[1])
815                     tran_text += trans[0] + "/" + total[0] + " " + total[1];
816                 else
817                     tran_text += trans.join(" ") + "/" + total.join(" ");
818             } else
819                 tran_text += trans.join(" ");
820         }
821         this.transferred_textnode.nodeValue = tran_text;
822         if (info.percent_complete >= 0) {
823             this.progress_container_node.style.display = "";
824             this.percent_textnode.nodeValue = info.percent_complete + "%";
825             this.progress_bar_node.style.width = info.percent_complete + "%";
826         } else {
827             this.percent_textnode.nodeValue = "";
828             this.progress_container_node.style.display = "none";
829         }
831         this.update_command_field();
832     },
834     update_time_field: function () {
835         var info = this.info;
836         var elapsed_text = pretty_print_time((Date.now() - info.start_time) / 1000) + " elapsed";
837         var text = "";
838         if (info.state == DOWNLOAD_DOWNLOADING)
839             text = pretty_print_file_size(info.speed).join(" ") + "/s, ";
840         if (info.state == DOWNLOAD_DOWNLOADING &&
841             info.size >= 0 &&
842             info.speed > 0)
843         {
844             let remaining = (info.size - info.amount_transferred) / info.speed;
845             text += pretty_print_time(remaining) + " left (" + elapsed_text + ")";
846         } else
847             text = elapsed_text;
848         this.time_textnode.nodeValue = text;
849     },
851     update_command_field: function () {
852         if (!this.generated)
853             return;
854         if (this.info.shell_command != null) {
855             this.command_div_node.style.display = "";
856             var label;
857             if (this.info.running_shell_command)
858                 label = "Running:";
859             else if (this.info.state == DOWNLOAD_FINISHED)
860                 label = "Ran command:";
861             else
862                 label = "Run command:";
863             this.command_label_textnode.nodeValue = label;
864             this.command_textnode.nodeValue = this.info.shell_command;
865         } else
866             this.command_div_node.style.display = "none";
867     }
870 function download_cancel (buffer) {
871     check_buffer(buffer, download_buffer);
872     var info = buffer.info;
873     yield info.cancel();
874     buffer.window.minibuffer.message("Download canceled");
876 interactive("download-cancel",
877     "Cancel the current download.\n" +
878     "The download can later be retried using the `download-retry' "+
879     "command, but any data already transferred will be lost.",
880     function (I) {
881         let result = yield I.window.minibuffer.read_single_character_option(
882             $prompt = "Cancel this download? (y/n)",
883             $options = ["y", "n"]);
884         if (result == "y")
885             yield download_cancel(I.buffer);
886     });
888 function download_retry (buffer) {
889     check_buffer(buffer, download_buffer);
890     var info = buffer.info;
891     yield info.retry();
892     buffer.window.minibuffer.message("Download retried");
894 interactive("download-retry",
895     "Retry a failed or canceled download.\n" +
896     "This command can be used to retry a download that failed or "+
897     "was canceled using the `download-cancel' command.  The download "+
898     "will begin from the start again.",
899     function (I) { yield download_retry(I.buffer); });
901 function download_pause (buffer) {
902     check_buffer(buffer, download_buffer);
903     yield buffer.info.pause();
904     buffer.window.minibuffer.message("Download paused");
906 interactive("download-pause",
907     "Pause the current download.\n" +
908     "The download can later be resumed using the `download-resume' command. "+
909     "The data already transferred will not be lost.",
910     function (I) { yield download_pause(I.buffer); });
912 function download_resume (buffer) {
913     check_buffer(buffer, download_buffer);
914     yield buffer.info.resume();
915     buffer.window.minibuffer.message("Download resumed");
917 interactive("download-resume",
918     "Resume the current download.\n" +
919     "This command can be used to resume a download paused using the "+
920     "`download-pause' command.",
921     function (I) { yield download_resume(I.buffer); });
923 function download_remove (buffer) {
924     check_buffer(buffer, download_buffer);
925     yield buffer.info.remove();
926     buffer.window.minibuffer.message("Download removed");
928 interactive("download-remove",
929     "Remove the current download from the download manager.\n" +
930     "This command can only be used on inactive (paused, canceled, "+
931     "completed, or failed) downloads.",
932     function (I) { yield download_remove(I.buffer); });
934 function download_retry_or_resume (buffer) {
935     check_buffer(buffer, download_buffer);
936     var info = buffer.info;
937     if (info.state == DOWNLOAD_PAUSED)
938         yield download_resume(buffer);
939     else
940         yield download_retry(buffer);
942 interactive("download-retry-or-resume",
943     "Retry or resume the current download.\n" +
944     "This command can be used to resume a download paused using the " +
945     "`download-pause' command or canceled using the `download-cancel' "+
946     "command.",
947     function (I) { yield download_retry_or_resume(I.buffer); });
949 function download_pause_or_resume (buffer) {
950     check_buffer(buffer, download_buffer);
951     var info = buffer.info;
952     if (info.state == DOWNLOAD_PAUSED)
953         yield download_resume(buffer);
954     else
955         yield download_pause(buffer);
957 interactive("download-pause-or-resume",
958     "Pause or resume the current download.\n" +
959     "This command toggles the paused state of the current download.",
960     function (I) { yield download_pause_or_resume(I.buffer); });
962 function download_delete_target (buffer) {
963     check_buffer(buffer, download_buffer);
964     var info = buffer.info;
965     info.delete_target();
966     buffer.window.minibuffer.message("Deleted file: " + info.target_file.path);
968 interactive("download-delete-target",
969     "Delete the target file of the current download.\n" +
970     "This command can only be used if the download has finished successfully.",
971     function (I) { download_delete_target(I.buffer); });
973 function download_shell_command (buffer, cwd, cmd) {
974     check_buffer(buffer, download_buffer);
975     var info = buffer.info;
976     if (info.state == DOWNLOAD_FINISHED) {
977         shell_command_with_argument_blind(cmd, info.target_file.path, $cwd = cwd);
978         return;
979     }
980     if (info.state != DOWNLOAD_DOWNLOADING &&
981         info.state != DOWNLOAD_PAUSED &&
982         info.state != DOWNLOAD_QUEUED)
983     {
984         info.throw_state_error();
985     }
986     if (cmd == null || cmd.length == 0)
987         info.set_shell_command(null, cwd);
988     else
989         info.set_shell_command(cmd, cwd);
990     buffer.window.minibuffer.message("Queued shell command: " + cmd);
992 interactive("download-shell-command",
993     "Run a shell command on the target file of the current download.\n"+
994     "If the download is still in progress, the shell command will be queued "+
995     "to run when the download finishes.",
996     function (I) {
997         var buffer = check_buffer(I.buffer, download_buffer);
998         var cwd = buffer.info.shell_command_cwd || I.local.cwd;
999         var cmd = yield I.minibuffer.read_shell_command(
1000             $cwd = cwd,
1001             $initial_value = buffer.info.shell_command ||
1002                 external_content_handlers.get(buffer.info.MIME_type));
1003         download_shell_command(buffer, cwd, cmd);
1004     });
1006 function download_manager_ui () {}
1007 download_manager_ui.prototype = {
1008     constructor: download_manager_ui,
1009     QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
1011     getAttention: function () {},
1012     show: function () {},
1013     visible: false
1017 interactive("download-manager-show-builtin-ui",
1018     "Show the built-in (Firefox-style) download manager window.",
1019     function (I) {
1020         Components.classesByID["{7dfdf0d1-aff6-4a34-bad1-d0fe74601642}"]
1021             .getService(Ci.nsIDownloadManagerUI)
1022             .show(I.window);
1023     });
1027  * Download-show
1028  */
1030 define_variable("download_temporary_file_open_buffer_delay", 500,
1031     "Delay (in milliseconds) before a download buffer is opened for "+
1032     "temporary downloads.  If the download completes before this amount "+
1033     "of time, no download buffer will be opened.  This variable takes "+
1034     "effect only if `open_download_buffer_automatically' is in "+
1035     "`download_added_hook', which is the case by default.");
1037 define_variable("download_buffer_automatic_open_target", OPEN_NEW_WINDOW,
1038     "Target(s) for download buffers created by "+
1039     "`open_download_buffer_automatically'.");
1041 minibuffer_auto_complete_preferences.download = true;
1043 function download_completer (completions) {
1044     keywords(arguments);
1045     if (! use_downloads_jsm) {
1046         completions = function (visitor) {
1047             var dls = download_manager_service.activeDownloads;
1048             while (dls.hasMoreElements()) {
1049                 let dl = dls.getNext();
1050                 visitor(dl);
1051             }
1052         };
1053     }
1054     all_word_completer.call(this, forward_keywords(arguments),
1055                             $completions = completions,
1056                             $get_string = function (x) {
1057                                 if (use_downloads_jsm)
1058                                     return x.target.path;
1059                                 else
1060                                     return x.displayName;
1061                             },
1062                             $get_description = function (x) {
1063                                 if (use_downloads_jsm)
1064                                     return x.source.url;
1065                                 else
1066                                     return x.source.spec
1067                             });
1069 download_completer.prototype = {
1070     constructor: download_completer,
1071     __proto__: all_word_completer.prototype,
1072     toString: function () "#<download_completer>"
1075 minibuffer.prototype.read_download = function () {
1076     keywords(arguments,
1077              $prompt = "Download",
1078              $auto_complete = "download",
1079              $auto_complete_initial = true,
1080              $require_match = true);
1081     if (use_downloads_jsm) {
1082         var list = yield Downloads.getList(Downloads.ALL);
1083         var all_downloads = yield list.getAll();
1084         var completer = new download_completer(all_downloads);
1085     } else {
1086         completer = new download_completer();
1087     }
1088     var result = yield this.read(forward_keywords(arguments),
1089                                  $completer = completer);
1090     yield co_return(result);
1094 function download_show (window, target, mozilla_info) {
1095     if (! window)
1096         target = OPEN_NEW_WINDOW;
1097     var info = lookup_download(mozilla_info);
1098     if (!info) {
1099         info = new download_info(null, null);
1100         info.attach(mozilla_info, true /* existing */);
1101     }
1102     create_buffer(window, buffer_creator(download_buffer, $info = info), target);
1105 function download_show_new_window (I) {
1106     var mozilla_info = yield I.minibuffer.read_download($prompt = "Show download:");
1107     download_show(I.window, OPEN_NEW_WINDOW, mozilla_info);
1110 function download_show_new_buffer (I) {
1111     var mozilla_info = yield I.minibuffer.read_download($prompt = "Show download:");
1112     download_show(I.window, OPEN_NEW_BUFFER, mozilla_info);
1115 function download_show_new_buffer_background (I) {
1116     var mozilla_info = yield I.minibuffer.read_download($prompt = "Show download:");
1117     download_show(I.window, OPEN_NEW_BUFFER_BACKGROUND, mozilla_info);
1120 function open_download_buffer_automatically (info) {
1121     var buf = info.source_buffer;
1122     var target = download_buffer_automatic_open_target;
1123     if (info.temporary_status == DOWNLOAD_NOT_TEMPORARY ||
1124         download_temporary_file_open_buffer_delay == 0)
1125     {
1126         download_show(buf ? buf.window : null, target, info.mozilla_info);
1127     } else {
1128         var timer = null;
1129         var finish = function finish () {
1130             timer.cancel();
1131         }
1132         add_hook.call(info, "download_finished_hook", finish);
1133         timer = call_after_timeout(function () {
1134             remove_hook.call(info, "download_finished_hook", finish);
1135             download_show(buf ? buf.window : null, target, info.mozilla_info);
1136         }, download_temporary_file_open_buffer_delay);
1137     }
1139 add_hook("download_added_hook", open_download_buffer_automatically);
1141 interactive("download-show",
1142     "Prompt for an ongoing download and open a download buffer showing "+
1143     "its progress.",
1144     alternates(download_show_new_buffer,
1145                download_show_new_window));
1147 provide("download-manager");