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