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