download-show: fix issue 265
[conkeror.git] / modules / download-manager.js
blob8d529d9a0ce01d5cd9b642256dfb94d3786c61d4
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 in_module(null);
11 require("special-buffer.js");
12 require("mime-type-override.js");
13 require("minibuffer-read-mime-type.js");
15 var download_manager_service = Cc["@mozilla.org/download-manager;1"]
16     .getService(Ci.nsIDownloadManager);
18 var download_manager_builtin_ui = Components
19     .classesByID["{7dfdf0d1-aff6-4a34-bad1-d0fe74601642}"]
20     .getService(Ci.nsIDownloadManagerUI);
23 var unmanaged_download_info_list = [];
24 var id_to_download_info = {};
26 // Import these constants for convenience
27 const DOWNLOAD_NOTSTARTED = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
28 const DOWNLOAD_DOWNLOADING = Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING;
29 const DOWNLOAD_FINISHED = Ci.nsIDownloadManager.DOWNLOAD_FINISHED;
30 const DOWNLOAD_FAILED = Ci.nsIDownloadManager.DOWNLOAD_FAILED;
31 const DOWNLOAD_CANCELED = Ci.nsIDownloadManager.DOWNLOAD_CANCELED;
32 const DOWNLOAD_PAUSED = Ci.nsIDownloadManager.DOWNLOAD_PAUSED;
33 const DOWNLOAD_QUEUED = Ci.nsIDownloadManager.DOWNLOAD_QUEUED;
34 const DOWNLOAD_BLOCKED = Ci.nsIDownloadManager.DOWNLOAD_BLOCKED;
35 const DOWNLOAD_SCANNING = Ci.nsIDownloadManager.DOWNLOAD_SCANNING;
38 const DOWNLOAD_NOT_TEMPORARY = 0;
39 const DOWNLOAD_TEMPORARY_FOR_ACTION = 1;
40 const DOWNLOAD_TEMPORARY_FOR_COMMAND = 2;
42 function download_info (source_buffer, mozilla_info, target_file) {
43     this.source_buffer = source_buffer;
44     this.target_file = target_file;
45     if (mozilla_info != null)
46         this.attach(mozilla_info);
48 download_info.prototype = {
49     attach : function (mozilla_info) {
50         if (!this.target_file)
51             this.__defineGetter__("target_file", function(){
52                     return this.mozilla_info.targetFile;
53                 });
54         else if (this.target_file.path != mozilla_info.targetFile.path)
55             throw interactive_error("Download target file unexpected.");
56         this.mozilla_info = mozilla_info;
57         id_to_download_info[mozilla_info.id] = this;
58         download_added_hook.run(this);
59     },
61     target_file : null,
63     shell_command : null,
65     shell_command_cwd : null,
67     temporary_status : DOWNLOAD_NOT_TEMPORARY,
69     action_description : null,
71     set_shell_command : function (str, cwd) {
72         this.shell_command = str;
73         this.shell_command_cwd = cwd;
74         if (this.mozilla_info)
75             download_shell_command_change_hook.run(this);
76     },
78     /**
79      * None of the following members may be used until attach is called
80      */
82     // Reflectors to properties of nsIDownload
83     get state () { return this.mozilla_info.state; },
84     get display_name () { return this.mozilla_info.displayName; },
85     get amount_transferred () { return this.mozilla_info.amountTransferred; },
86     get percent_complete () { return this.mozilla_info.percentComplete; },
87     get size () {
88         var s = this.mozilla_info.size;
89         /* nsIDownload.size is a PRUint64, and will have value
90          * LL_MAXUINT (2^64 - 1) to indicate an unknown size.  Because
91          * JavaScript only has a double numerical type, this value
92          * cannot be represented exactly, so 2^36 is used instead as the cutoff. */
93         if (s < 68719476736 /* 2^36 */)
94             return s;
95         return -1;
96     },
97     get source () { return this.mozilla_info.source; },
98     get start_time () { return this.mozilla_info.startTime; },
99     get speed () { return this.mozilla_info.speed; },
100     get MIME_info () { return this.mozilla_info.MIMEInfo; },
101     get MIME_type () {
102         if (this.MIME_info)
103             return this.MIME_info.MIMEType;
104         return null;
105     },
106     get id () { return this.mozilla_info.id; },
107     get referrer () { return this.mozilla_info.referrer; },
109     target_file_text : function () {
110         let target = this.target_file.path;
111         let display = this.display_name;
112         if (target.indexOf(display, target.length - display.length) == -1)
113             target += " (" + display + ")";
114         return target;
115     },
117     throw_if_removed : function () {
118         if (this.removed)
119             throw interactive_error("Download has already been removed from the download manager.");
120     },
122     throw_state_error : function () {
123         switch (this.state) {
124         case DOWNLOAD_DOWNLOADING:
125             throw interactive_error("Download is already in progress.");
126         case DOWNLOAD_FINISHED:
127             throw interactive_error("Download has already completed.");
128         case DOWNLOAD_FAILED:
129             throw interactive_error("Download has already failed.");
130         case DOWNLOAD_CANCELED:
131             throw interactive_error("Download has already been canceled.");
132         case DOWNLOAD_PAUSED:
133             throw interactive_error("Download has already been paused.");
134         case DOWNLOAD_QUEUED:
135             throw interactive_error("Download is queued.");
136         default:
137             throw new Error("Download has unexpected state: " + this.state);
138         }
139     },
141     // Download manager operations
142     cancel : function ()  {
143         this.throw_if_removed();
144         switch (this.state) {
145         case DOWNLOAD_DOWNLOADING:
146         case DOWNLOAD_PAUSED:
147         case DOWNLOAD_QUEUED:
148             try {
149                 download_manager_service.cancelDownload(this.id);
150             } catch (e) {
151                 throw interactive_error("Download cannot be canceled.");
152             }
153             break;
154         default:
155             this.throw_state_error();
156         }
157     },
159     retry : function () {
160         this.throw_if_removed();
161         switch (this.state) {
162         case DOWNLOAD_CANCELED:
163         case DOWNLOAD_FAILED:
164             try {
165                 download_manager_service.retryDownload(this.id);
166             } catch (e) {
167                 throw interactive_error("Download cannot be retried.");
168             }
169             break;
170         default:
171             this.throw_state_error();
172         }
173     },
175     resume : function () {
176         this.throw_if_removed();
177         switch (this.state) {
178         case DOWNLOAD_PAUSED:
179             try {
180                 download_manager_service.resumeDownload(this.id);
181             } catch (e) {
182                 throw interactive_error("Download cannot be resumed.");
183             }
184             break;
185         default:
186             this.throw_state_error();
187         }
188     },
190     pause : function () {
191         this.throw_if_removed();
192         switch (this.state) {
193         case DOWNLOAD_DOWNLOADING:
194         case DOWNLOAD_QUEUED:
195             try {
196                 download_manager_service.pauseDownload(this.id);
197             } catch (e) {
198                 throw interactive_error("Download cannot be paused.");
199             }
200             break;
201         default:
202             this.throw_state_error();
203         }
204     },
206     remove : function () {
207         this.throw_if_removed();
208         switch (this.state) {
209         case DOWNLOAD_FAILED:
210         case DOWNLOAD_CANCELED:
211         case DOWNLOAD_FINISHED:
212             try {
213                 download_manager_service.removeDownload(this.id);
214             } catch (e) {
215                 throw interactive_error("Download cannot be removed.");
216             }
217             break;
218         default:
219             throw interactive_error("Download is still in progress.");
220         }
221     },
223     delete_target : function () {
224         if (this.state != DOWNLOAD_FINISHED)
225             throw interactive_error("Download has not finished.");
226         try {
227             this.target_file.remove(false);
228         } catch (e) {
229             if ("result" in e) {
230                 switch (e.result) {
231                 case Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
232                     throw interactive_error("File has already been deleted.");
233                 case Cr.NS_ERROR_FILE_ACCESS_DENIED:
234                     throw interactive_error("Access denied");
235                 case Cr.NS_ERROR_FILE_DIR_NOT_EMPTY:
236                     throw interactive_error("Failed to delete file.");
237                 }
238             }
239             throw e;
240         }
241     }
244 var define_download_local_hook = simple_local_hook_definer();
246 // FIXME: add more parameters
247 function register_download (buffer, source_uri, target_file) {
248     var info = new download_info(buffer, null, target_file);
249     info.registered_time_stamp = Date.now();
250     info.registered_source_uri = source_uri;
251     unmanaged_download_info_list.push(info);
252     return info;
255 function match_registered_download (mozilla_info) {
256     let list = unmanaged_download_info_list;
257     let t = Date.now();
258     for (let i = 0; i < list.length; ++i) {
259         let x = list[i];
260         if (x.registered_source_uri == mozilla_info.source) {
261             list.splice(i, 1);
262             return x;
263         }
264         if (t - x.registered_time_stamp > download_info_max_queue_delay) {
265             list.splice(i, 1);
266             --i;
267             continue;
268         }
269     }
270     return null;
273 define_download_local_hook("download_added_hook");
274 define_download_local_hook("download_removed_hook");
275 define_download_local_hook("download_finished_hook");
276 define_download_local_hook("download_progress_change_hook");
277 define_download_local_hook("download_state_change_hook");
278 define_download_local_hook("download_shell_command_change_hook");
280 define_variable('delete_temporary_files_for_command', true,
281     'If this is set to true, temporary files downloaded to run a command '+
282     'on them will be deleted once the command completes. If not, the file '+
283     'will stay around forever unless deleted outside the browser.');
285 var download_info_max_queue_delay = 100;
287 var download_progress_listener = {
288     QueryInterface: generate_QI(Ci.nsIDownloadProgressListener),
290     onDownloadStateChange : function (state, download) {
291         var info = null;
292         /* FIXME: Determine if only new downloads will have this state
293          * as their previous state. */
295         dumpln("download state change: " + download.source.spec + ": " + state + ", " + download.state + ", " + download.id);
297         if (state == DOWNLOAD_NOTSTARTED) {
298             info = match_registered_download(download);
299             if (info == null) {
300                 info = new download_info(null, download);
301                 dumpln("error: encountered unknown new download");
302             } else {
303                 info.attach(download);
304             }
305         } else {
306             info = id_to_download_info[download.id];
307             if (info == null) {
308                 dumpln("Error: encountered unknown download");
310             } else {
311                 info.mozilla_info = download;
312                 download_state_change_hook.run(info);
313                 if (info.state == DOWNLOAD_FINISHED) {
314                     download_finished_hook.run(info);
316                     if (info.shell_command != null) {
317                         info.running_shell_command = true;
318                         co_call(function () {
319                             try {
320                                 yield shell_command_with_argument(info.shell_command,
321                                                                   info.target_file.path,
322                                                                   $cwd = info.shell_command_cwd);
323                             } catch (e) {
324                                 handle_interactive_error(info.source_buffer.window, e);
325                             } finally  {
326                                 if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
327                                     if(delete_temporary_files_for_command) {
328                                         info.target_file.remove(false /* not recursive */);
329                                     }
330                                 info.running_shell_command = false;
331                                 download_shell_command_change_hook.run(info);
332                             }
333                         }());
334                         download_shell_command_change_hook.run(info);
335                     }
336                 }
337             }
338         }
339     },
341     onProgressChange : function (progress, request, cur_self_progress, max_self_progress,
342                                  cur_total_progress, max_total_progress,
343                                  download) {
344         var info = id_to_download_info[download.id];
345         if (info == null) {
346             dumpln("error: encountered unknown download in progress change");
347             return;
348         }
349         info.mozilla_info = download;
350         download_progress_change_hook.run(info);
351         //dumpln("download progress change: " + download.source.spec + ": " + cur_self_progress + "/" + max_self_progress + " "
352         // + cur_total_progress + "/" + max_total_progress + ", " + download.state + ", " + download.id);
353     },
355     onSecurityChange : function (progress, request, state, download) {
356     },
358     onStateChange : function (progress, request, state_flags, status, download) {
359     }
362 var download_observer = {
363     observe : function (subject, topic, data) {
364         switch(topic) {
365         case "download-manager-remove-download":
366             var ids = [];
367             if (!subject) {
368                 // Remove all downloads
369                 for (let i in id_to_download_info)
370                     ids.push(i);
371             } else {
372                 let id = subject.QueryInterface(Ci.nsISupportsPRUint32);
373                 /* FIXME: determine if this should really be an error */
374                 if (!(id in id_to_download_info)) {
375                     dumpln("Error: download-manager-remove-download event received for unknown download: " + id);
376                 } else
377                     ids.push(id);
378             }
379             for each (let i in ids) {
380                 dumpln("deleting download: " + i);
381                 let d = id_to_download_info[i];
382                 d.removed = true;
383                 download_removed_hook.run(d);
384                 delete id_to_download_info[i];
385             }
386             break;
387         }
388     }
390 observer_service.addObserver(download_observer, "download-manager-remove-download", false);
392 download_manager_service.addListener(download_progress_listener);
394 define_variable("download_buffer_min_update_interval", 2000,
395     "Minimum interval (in milliseconds) between updates in download progress buffers.\n" +
396     "Lowering this interval will increase the promptness of the progress display at " +
397     "the cost of using additional processor time.");
399 function download_buffer_modality (buffer, element) {
400     buffer.keymaps.push(download_buffer_keymap);
403 define_keywords("$info");
404 function download_buffer (window, element) {
405     this.constructor_begin();
406     keywords(arguments);
407     special_buffer.call(this, window, element, forward_keywords(arguments));
408     this.info = arguments.$info;
409     this.local.cwd = this.info.mozilla_info.targetFile.parent;
410     this.description = this.info.mozilla_info.source.spec;
411     this.update_title();
413     this.progress_change_handler_fn = method_caller(this, this.handle_progress_change);
414     add_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
415     add_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
416     this.command_change_handler_fn = method_caller(this, this.update_command_field);
417     add_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
418     this.modalities.push(download_buffer_modality);
419     this.constructor_end();
421 download_buffer.prototype = {
422     __proto__: special_buffer.prototype,
424     handle_kill : function () {
425         special_buffer.prototype.handle_kill.call(this);
426         remove_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
427         remove_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
428         remove_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
430         // Remove all node references
431         delete this.status_textnode;
432         delete this.target_file_node;
433         delete this.transferred_div_node;
434         delete this.transferred_textnode;
435         delete this.progress_container_node;
436         delete this.progress_bar_node;
437         delete this.percent_textnode;
438         delete this.time_textnode;
439         delete this.command_div_node;
440         delete this.command_label_textnode;
441         delete this.command_textnode;
442     },
444     update_title : function () {
445         // FIXME: do this properly
446         var new_title;
447         var info = this.info;
448         var append_transfer_info = false;
449         var append_speed_info = true;
450         var label = null;
451         switch(info.state) {
452         case DOWNLOAD_DOWNLOADING:
453             label = "Downloading";
454             append_transfer_info = true;
455             break;
456         case DOWNLOAD_FINISHED:
457             label = "Download complete";
458             break;
459         case DOWNLOAD_FAILED:
460             label = "Download failed";
461             append_transfer_info = true;
462             append_speed_info = false;
463             break;
464         case DOWNLOAD_CANCELED:
465             label = "Download canceled";
466             append_transfer_info = true;
467             append_speed_info = false;
468             break;
469         case DOWNLOAD_PAUSED:
470             label = "Download paused";
471             append_transfer_info = true;
472             append_speed_info = false;
473             break;
474         case DOWNLOAD_QUEUED:
475         default:
476             label = "Download queued";
477             break;
478         }
480         if (append_transfer_info) {
481             if (append_speed_info)
482                 new_title = label + " at " + pretty_print_file_size(info.speed).join(" ") + "/s: ";
483             else
484                 new_title = label + ": ";
485             var trans = pretty_print_file_size(info.amount_transferred);
486             if (info.size >= 0) {
487                 var total = pretty_print_file_size(info.size);
488                 if (trans[1] == total[1])
489                     new_title += trans[0] + "/" + total[0] + " " + total[1];
490                 else
491                     new_title += trans.join(" ") + "/" + total.join(" ");
492             } else
493                 new_title += trans.join(" ");
494             if (info.percent_complete >= 0)
495                 new_title += " (" + info.percent_complete + "%)";
496         } else
497             new_title = label;
498         if (new_title != this.title) {
499             this.title = new_title;
500             return true;
501         }
502         return false;
503     },
505     handle_progress_change : function () {
506         var cur_time = Date.now();
507         if (this.last_update == null ||
508             (cur_time - this.last_update) > download_buffer_min_update_interval ||
509             this.info.state != this.previous_state) {
511             if (this.update_title())
512                 buffer_title_change_hook.run(this);
514             if (this.generated) {
515                 this.update_fields();
516             }
517             this.previous_status = this.info.status;
518             this.last_update = cur_time;
519         }
520     },
522     generate : function () {
523         var d = this.document;
524         var g = new dom_generator(d, XHTML_NS);
526         /* Warning: If any additional node references are saved in
527          * this function, appropriate code to delete the saved
528          * properties must be added to handle_kill. */
530         var info = this.info;
532         d.body.setAttribute("class", "download-buffer");
534         g.add_stylesheet("chrome://conkeror-gui/content/downloads.css");
536         var row, cell;
537         var table = g.element("table", d.body);
539         row = g.element("tr", table, "class", "download-info", "id", "download-source");
540         cell = g.element("td", row, "class", "download-label");
541         this.status_textnode = g.text("", cell);
542         cell = g.element("td", row, "class", "download-value");
543         g.text(info.source.spec, cell);
545         row = g.element("tr", table, "class", "download-info", "id", "download-target");
546         cell = g.element("td", row, "class", "download-label");
547         var target_label;
548         if (info.temporary_status != DOWNLOAD_NOT_TEMPORARY)
549             target_label = "Temp. file:";
550         else
551             target_label = "Target:";
552         g.text(target_label, cell);
553         cell = g.element("td", row, "class", "download-value");
554         this.target_file_node = g.text("", cell);
556         row = g.element("tr", table, "class", "download-info", "id", "download-mime-type");
557         cell = g.element("td", row, "class", "download-label");
558         g.text("MIME type:", cell);
559         cell = g.element("td", row, "class", "download-value");
560         g.text(info.MIME_type || "unknown", cell);
562         this.transferred_div_node = row =
563             g.element("tr", table, "class", "download-info", "id", "download-transferred");
564         cell = g.element("td", row, "class", "download-label");
565         g.text("Transferred:", cell);
566         cell = g.element("td", row, "class", "download-value");
567         var sub_item = g.element("div", cell);
568         this.transferred_textnode = g.text("", sub_item);
569         sub_item = g.element("div", cell, "id", "download-percent");
570         this.percent_textnode = g.text("", sub_item);
571         this.progress_container_node = sub_item = g.element("div", cell, "id", "download-progress-container");
572         this.progress_bar_node = g.element("div", sub_item, "id", "download-progress-bar");
574         row = g.element("tr", table, "class", "download-info", "id", "download-time");
575         cell = g.element("td", row, "class", "download-label");
576         g.text("Time:", cell);
577         cell = g.element("td", row, "class", "download-value");
578         this.time_textnode = g.text("", cell);
580         if (info.action_description != null) {
581             row = g.element("tr", table, "class", "download-info", "id", "download-action");
582             cell = g.element("div", row, "class", "download-label");
583             g.text("Action:", cell);
584             cell = g.element("div", row, "class", "download-value");
585             g.text(info.action_description, cell);
586        }
588         this.command_div_node = row = g.element("tr", table, "class", "download-info", "id", "download-command");
589         cell = g.element("td", row, "class", "download-label");
590         this.command_label_textnode = g.text("Run command:", cell);
591         cell = g.element("td", row, "class", "download-value");
592         this.command_textnode = g.text("", cell);
595         this.update_fields();
596         this.update_command_field();
597     },
599     update_fields : function () {
600         if (!this.generated)
601             return;
602         var info = this.info;
603         var label = null;
604         switch(info.state) {
605         case DOWNLOAD_DOWNLOADING:
606             label = "Downloading";
607             break;
608         case DOWNLOAD_FINISHED:
609             label = "Completed";
610             break;
611         case DOWNLOAD_FAILED:
612             label = "Failed";
613             break;
614         case DOWNLOAD_CANCELED:
615             label = "Canceled";
616             break;
617         case DOWNLOAD_PAUSED:
618             label = "Paused";
619             break;
620         case DOWNLOAD_QUEUED:
621         default:
622             label = "Queued";
623             break;
624         }
625         this.status_textnode.nodeValue = label + ":";
626         this.target_file_node.nodeValue = info.target_file_text();
627         this.update_time_field();
629         var tran_text = "";
630         if (info.state == DOWNLOAD_FINISHED)
631             tran_text = pretty_print_file_size(info.size).join(" ");
632         else {
633             var trans = pretty_print_file_size(info.amount_transferred);
634             if (info.size >= 0) {
635                 var total = pretty_print_file_size(info.size);
636                 if (trans[1] == total[1])
637                     tran_text += trans[0] + "/" + total[0] + " " + total[1];
638                 else
639                     tran_text += trans.join(" ") + "/" + total.join(" ");
640             } else
641                 tran_text += trans.join(" ");
642         }
643         this.transferred_textnode.nodeValue = tran_text;
644         if (info.percent_complete >= 0) {
645             this.progress_container_node.style.display = "";
646             this.percent_textnode.nodeValue = info.percent_complete + "%";
647             this.progress_bar_node.style.width = info.percent_complete + "%";
648         } else {
649             this.percent_textnode.nodeValue = "";
650             this.progress_container_node.style.display = "none";
651         }
653         this.update_command_field();
654     },
656     update_time_field : function () {
657         var info = this.info;
658         var elapsed_text = pretty_print_time((Date.now() - info.start_time / 1000) / 1000) + " elapsed";
659         var text = "";
660         if (info.state == DOWNLOAD_DOWNLOADING) {
661             text = pretty_print_file_size(info.speed).join(" ") + "/s, ";
662         }
663         if (info.state == DOWNLOAD_DOWNLOADING &&
664             info.size >= 0 &&
665             info.speed > 0) {
666             let remaining = (info.size - info.amount_transferred) / info.speed;
667             text += pretty_print_time(remaining) + " left (" + elapsed_text + ")";
668         } else {
669             text = elapsed_text;
670         }
671         this.time_textnode.nodeValue = text;
672     },
674     update_command_field : function () {
675         if (!this.generated)
676             return;
677         if (this.info.shell_command != null) {
678             this.command_div_node.style.display = "";
679             var label;
680             if (this.info.running_shell_command)
681                 label = "Running:";
682             else if (this.info.state == DOWNLOAD_FINISHED)
683                 label = "Ran command:";
684             else
685                 label = "Run command:";
686             this.command_label_textnode.nodeValue = label;
687             this.command_textnode.nodeValue = this.info.shell_command;
688         } else {
689             this.command_div_node.style.display = "none";
690         }
691     }
694 function download_cancel (buffer) {
695     check_buffer(buffer, download_buffer);
696     var info = buffer.info;
697     info.cancel();
698     buffer.window.minibuffer.message("Download canceled");
700 interactive("download-cancel",
701             "Cancel the current download.\n" +
702             "The download can later be retried using the `download-retry' command, but any " +
703             "data already transferred will be lost.",
704             function (I) {
705                 let result = yield I.window.minibuffer.read_single_character_option(
706                     $prompt = "Cancel this download? (y/n)",
707                     $options = ["y", "n"]);
708                 if (result == "y")
709                     download_cancel(I.buffer);
710             });
712 function download_retry (buffer) {
713     check_buffer(buffer, download_buffer);
714     var info = buffer.info;
715     info.retry();
716     buffer.window.minibuffer.message("Download retried");
718 interactive("download-retry",
719             "Retry a failed or canceled download.\n" +
720             "This command can be used to retry a download that failed or was canceled using " +
721             "the `download-cancel' command.  The download will begin from the start again.",
722             function (I) {download_retry(I.buffer);});
724 function download_pause (buffer) {
725     check_buffer(buffer, download_buffer);
726     buffer.info.pause();
727     buffer.window.minibuffer.message("Download paused");
729 interactive("download-pause",
730             "Pause the current download.\n" +
731             "The download can later be resumed using the `download-resume' command.  The " +
732             "data already transferred will not be lost.",
733             function (I) {download_pause(I.buffer);});
735 function download_resume (buffer) {
736     check_buffer(buffer, download_buffer);
737     buffer.info.resume();
738     buffer.window.minibuffer.message("Download resumed");
740 interactive("download-resume",
741             "Resume the current download.\n" +
742             "This command can be used to resume a download paused using the `download-pause' command.",
743             function (I) { download_resume(I.buffer); });
745 function download_remove (buffer) {
746     check_buffer(buffer, download_buffer);
747     buffer.info.remove();
748     buffer.window.minibuffer.message("Download removed");
750 interactive("download-remove",
751             "Remove the current download from the download manager.\n" +
752             "This command can only be used on inactive (paused, canceled, "+
753             "completed, or failed) downloads.",
754             function (I) {download_remove(I.buffer);});
756 function download_retry_or_resume (buffer) {
757     check_buffer(buffer, download_buffer);
758     var info = buffer.info;
759     if (info.state == DOWNLOAD_PAUSED)
760         download_resume(buffer);
761     else
762         download_retry(buffer);
764 interactive("download-retry-or-resume",
765             "Retry or resume the current download.\n" +
766             "This command can be used to resume a download paused using the `download-pause' " +
767             "command or canceled using the `download-cancel' command.",
768             function (I) {download_retry_or_resume(I.buffer);});
770 function download_pause_or_resume (buffer) {
771     check_buffer(buffer, download_buffer);
772     var info = buffer.info;
773     if (info.state == DOWNLOAD_PAUSED)
774         download_resume(buffer);
775     else
776         download_pause(buffer);
778 interactive("download-pause-or-resume",
779             "Pause or resume the current download.\n" +
780             "This command toggles the paused state of the current download.",
781             function (I) {download_pause_or_resume(I.buffer);});
783 function download_delete_target (buffer) {
784     check_buffer(buffer, download_buffer);
785     var info = buffer.info;
786     info.delete_target();
787     buffer.window.minibuffer.message("Deleted file: " + info.target_file.path);
789 interactive("download-delete-target",
790             "Delete the target file of the current download.\n"  +
791             "This command can only be used if the download has finished successfully.",
792             function (I) {download_delete_target(I.buffer);});
794 function download_shell_command (buffer, cwd, cmd) {
795     check_buffer(buffer, download_buffer);
796     var info = buffer.info;
797     if (info.state == DOWNLOAD_FINISHED) {
798         shell_command_with_argument_blind(cmd, info.target_file.path, $cwd = cwd);
799         return;
800     }
801     if (info.state != DOWNLOAD_DOWNLOADING && info.state != DOWNLOAD_PAUSED && info.state != DOWNLOAD_QUEUED)
802         info.throw_state_error();
803     if (cmd == null || cmd.length == 0)
804         info.set_shell_command(null, cwd);
805     else
806         info.set_shell_command(cmd, cwd);
807     buffer.window.minibuffer.message("Queued shell command: " + cmd);
809 interactive("download-shell-command",
810             "Run a shell command on the target file of the current download.\n" +
811             "If the download is still in progress, the shell command will be queued " +
812             "to run when the download finishes.",
813             function (I) {
814                 var buffer = check_buffer(I.buffer, download_buffer);
815                 var cwd = buffer.info.shell_command_cwd || I.local.cwd;
816                 var cmd = yield I.minibuffer.read_shell_command(
817                     $cwd = cwd,
818                     $initial_value = buffer.info.shell_command ||
819                         external_content_handlers.get(buffer.info.MIME_type));
820                 download_shell_command(buffer, cwd, cmd);
821             });
823 function download_manager_ui () {}
824 download_manager_ui.prototype = {
825     QueryInterface : XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
827     getAttention : function () {},
828     show : function () {},
829     visible : false
833 function download_manager_show_builtin_ui (window) {
834     download_manager_builtin_ui.show(window);
836 interactive("download-manager-show-builtin-ui",
837             "Show the built-in (Firefox-style) download manager user interface.",
838             function (I) {download_manager_show_builtin_ui(I.window);});
841 define_variable("download_temporary_file_open_buffer_delay", 500,
842     "Delay (in milliseconds) before a download buffer is opened for "+
843     "temporary downloads.  If the download completes before this amount "+
844     "of time, no download buffer will be opened.  This variable takes "+
845     "effect only if `open_download_buffer_automatically' is in "+
846     "`download_added_hook', which is the case by default.");
849 define_variable("download_buffer_automatic_open_target",
850                 [OPEN_NEW_WINDOW, OPEN_NEW_BUFFER_BACKGROUND],
851     "Target(s) for download buffers created by "+
852     "`open_download_buffer_automatically' and `download-show'.\n"+
853     "It can be a single target or an array of two targets.  When it is an "+
854     "array, the `download-show' command will use the second target when "+
855     "called with universal-argument.");
858 function open_download_buffer_automatically (info, target, buffer) {
859     var buf = buffer || info.source_buffer;
860     if (target == null) {
861         if (typeof(download_buffer_automatic_open_target) == "object")
862             target = download_buffer_automatic_open_target[0];
863         else
864             target = download_buffer_automatic_open_target;
865     }
866     if (buf == null)
867         target = OPEN_NEW_WINDOW;
868     if (info.temporary_status == DOWNLOAD_NOT_TEMPORARY ||
869         download_temporary_file_open_buffer_delay == 0)
870     {
871         create_buffer(buf.window, buffer_creator(download_buffer, $info = info), target);
872     } else {
873         var timer = null;
874         function finish () {
875             timer.cancel();
876         }
877         add_hook.call(info, "download_finished_hook", finish);
878         timer = call_after_timeout(function () {
879                 remove_hook.call(info, "download_finished_hook", finish);
880                 create_buffer(buf.window, buffer_creator(download_buffer, $info = info), target);
881             }, download_temporary_file_open_buffer_delay);
882     }
884 add_hook("download_added_hook", open_download_buffer_automatically);
888  * Download-show
889  */ 
891 minibuffer_auto_complete_preferences.download = true;
893 minibuffer.prototype.read_download = function () {
894     keywords(arguments,
895              $prompt = "Download",
896              $completer = all_word_completer(
897                  $completions = function (visitor) {
898                      var dls = download_manager_service.activeDownloads;
899                      while (dls.hasMoreElements()) {
900                          let dl = dls.getNext();
901                          visitor(id_to_download_info[dl.id]);
902                      }
903                  },
904                  $get_string = function (x) x.display_name,
905                  $get_description = function (x) x.source.spec,
906                  $get_value = function (x) x),
907              $auto_complete = "download",
908              $auto_complete_initial = true,
909              $match_required = true);
910     var result = yield this.read(forward_keywords(arguments));
911     yield co_return(result);
914 interactive("download-show",
915     "Prompt for an ongoing download and open a download buffer showing "+
916     "its progress.  When called with universal argument, the second "+
917     "target from `download_buffer_automatic_open_target' will be used.",
918     function (I) {
919         var target = null;
920         if (I.P && typeof(download_buffer_automatic_open_target) == "object")
921             target = download_buffer_automatic_open_target[1];
922         open_download_buffer_automatically(
923             (yield I.minibuffer.read_download($prompt = "Show download:")),
924             target,
925             I.buffer);
926     });
928 provide("download-manager");