root/trunk/chrome/content/dta/manager.js

Revision 904, 64.8 kB (checked in by MaierMan, 2 years ago)

alternate prealloc method

Line 
1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3  *
4  * The contents of this file are subject to the Mozilla Public License Version
5  * 1.1 (the "License"); you may not use this file except in compliance with
6  * the License. You may obtain a copy of the License at
7  * http://www.mozilla.org/MPL/
8  *
9  * Software distributed under the License is distributed on an "AS IS" basis,
10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11  * for the specific language governing rights and limitations under the
12  * License.
13  *
14  * The Original Code is DownThemAll!
15  *
16  * The Initial Developers of the Original Code are Stefano Verna and Federico Parodi
17  * Portions created by the Initial Developers are Copyright (C) 2004-2007
18  * the Initial Developers. All Rights Reserved.
19  *
20  * Contributor(s):
21  *    Stefano Verna <stefano.verna@gmail.com>
22  *    Federico Parodi <f.parodi@tiscali.it>
23  *    Nils Maier <MaierMan@web.de>
24  *
25  * Alternatively, the contents of this file may be used under the terms of
26  * either the GNU General Public License Version 2 or later (the "GPL"), or
27  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28  * in which case the provisions of the GPL or the LGPL are applicable instead
29  * of those above. If you wish to allow use of your version of this file only
30  * under the terms of either the GPL or the LGPL, and not to allow others to
31  * use your version of this file under the terms of the MPL, indicate your
32  * decision by deleting the provisions above and replace them with the notice
33  * and other provisions required by the GPL or the LGPL. If you do not delete
34  * the provisions above, a recipient may use your version of this file under
35  * the terms of any one of the MPL, the GPL or the LGPL.
36  *
37  * ***** END LICENSE BLOCK ***** */
38  
39 const NS_DTA = 'http://www.downthemall.net/properties#';
40 const NS_METALINKER = 'http://www.metalinker.org/';
41  
42  
43 const NS_ERROR_MODULE_NETWORK = 0x804B0000;
44 const NS_ERROR_BINDING_ABORTED = NS_ERROR_MODULE_NETWORK + 2;
45 const NS_ERROR_UNKNOWN_HOST = NS_ERROR_MODULE_NETWORK + 30;
46 const NS_ERROR_CONNECTION_REFUSED = NS_ERROR_MODULE_NETWORK + 13;
47 const NS_ERROR_NET_TIMEOUT = NS_ERROR_MODULE_NETWORK + 14;
48 const NS_ERROR_NET_RESET = NS_ERROR_MODULE_NETWORK + 20;
49
50 const Cc = Components.classes;
51 const Ci = Components.interfaces;
52
53 const Exception = Components.Exception;
54 const Construct = Components.Constructor;
55 function Serv(c, i) {
56   return Cc[c].getService(i ? Ci[i] : null);
57 }
58
59 const BufferedOutputStream = Construct('@mozilla.org/network/buffered-output-stream;1', 'nsIBufferedOutputStream', 'init');
60 const BinaryOutputStream = Construct('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream');
61 const BinaryInputStream = Construct('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream');
62 const FileInputStream = Construct('@mozilla.org/network/file-input-stream;1', 'nsIFileInputStream', 'init');
63 const StringInputStream = Construct('@mozilla.org/io/string-input-stream;1', 'nsIStringInputStream', 'setData');
64
65 const ContentHandling = Serv('@downthemall.net/contenthandling;1', 'dtaIContentHandling');
66 const MimeService = Serv('@mozilla.org/uriloader/external-helper-app-service;1', 'nsIMIMEService');
67 const ObserverService = Serv('@mozilla.org/observer-service;1', 'nsIObserverService');
68 const WindowWatcherService = Serv('@mozilla.org/embedcomp/window-watcher;1', 'nsIWindowWatcher');
69
70 const MIN_CHUNK_SIZE = 512 * 1024;
71
72 // ammount to buffer in BufferedOutputStream
73 // furthermore up to this ammount will automagically discared after crashes
74 const CHUNK_BUFFER_SIZE = 96 * 1024;
75
76 // in use by chunk.writer...
77 // in use by decompressor... beware, actual size might be more than twice as big!
78 const MAX_BUFFER_SIZE = 5 * 1024 * 1024;
79 const MIN_BUFFER_SIZE = 1 * 1024 * 1024;
80
81 const REFRESH_FREQ = 1000;
82 const REFRESH_NFREQ = 1000 / REFRESH_FREQ;
83 const STREAMS_FREQ = 200;
84
85 var Dialog = {
86   _observes: ['quit-application-requested', 'quit-application-granted'],
87   _initialized: false,
88   _wasRunning: false,
89   _lastTime: Utils.getTimestamp(),
90   _running: [],
91   _autoClears: [],
92   completed: 0,
93   totalbytes: 0,
94   init: function D_init() {
95     Tree.init($("downloads"));
96     makeObserver(this);
97    
98     this._observes.forEach(
99       function(topic) {
100         ObserverService.addObserver(this, topic, true);
101       },
102       this
103     );
104  
105     document.getElementById("dtaHelp").hidden = !("openHelp" in window);
106  
107     SessionManager.init();
108  
109     if ("arguments" in window) {
110       startDownloads(window.arguments[0], window.arguments[1]);
111     }
112
113     Tree.invalidate();
114     this._initialized = true;
115     for (let d in Tree.all) {
116       if (d.is(FINISHING)) {
117         this.run(d);
118       }
119     }
120     this._updTimer = new Timer("Dialog.checkDownloads();", REFRESH_FREQ, true, true);
121     new Timer("Dialog.refreshWritten();", 100, true, true);
122     new Timer("Dialog.saveRunning();", 10000, true);
123   },
124   observe: function D_observe(subject, topic, data) {
125     if (topic == 'quit-application-requested') {
126       if (!this._canClose()) {
127         delete this._forceClose;
128         try {
129           let cancelQuit = subject.QueryInterface(Ci.nsISupportsPRBool);
130           cancelQuit.data = true;
131         }
132         catch (ex) {
133           Debug.log("cannot set cancelQuit", ex);
134         }
135       }
136     }
137     else if (topic == 'quit-application-granted') {
138       this._forceClose = true;
139     }
140   },
141   refresh: function D_refresh() {
142     try {
143       let sum = 0;
144       const now = Utils.getTimestamp();
145       this._running.forEach(
146         function(i) {
147           let d = i.d;
148          
149           let advanced = (d.partialSize - i.lastBytes);
150           sum += advanced;
151          
152           let elapsed = (now - i.lastTime) / 1000;         
153           if (elapsed < 1) {
154             return;
155           }           
156          
157           let speed = Math.round(advanced / elapsed);
158          
159           i.lastBytes = d.partialSize;
160           i.lastTime = now;       
161
162           // Refresh item speed
163           d.speeds.push(speed > 0 ? speed : 0);
164           if (d.speeds.length > SPEED_COUNT) {
165             d.speeds.shift();
166           }
167           i.lastBytes = d.partialSize;
168           i.lastTime = now;
169          
170           speed = 0;
171           d.speeds.forEach(
172             function(s) {
173               speed += s;
174             }
175           );
176           speed /= d.speeds.length;
177          
178           // Calculate estimated time         
179           if (advanced != 0 && d.totalSize > 0) {
180             let remaining = Math.ceil((d.totalSize - d.partialSize) / speed);
181             if (!isFinite(remaining)) {
182               d.status = _("unknown");
183             }
184             else {
185               d.status = Utils.formatTimeDelta(remaining);
186             }
187           }
188           d.speed = Utils.formatBytes(speed) + "/s";
189         }
190       );
191       let elapsed = (now - this._lastTime) / 1000;
192       this._lastTime = now;
193       let speed = Math.round(sum * elapsed);
194       speed = Utils.formatBytes((speed > 0) ? speed : 0);
195
196       // Refresh status bar
197       $("statusText").label =
198         _("cdownloads", [this.completed, Tree.rowCount])
199         + " - "
200         + _("cspeed")
201         + " "
202         + speed + "/s";
203
204       // Refresh window title
205       if (this._running.length == 1 && this._running[0].d.totalSize > 0) {
206         document.title =
207           this._running[0].d.percent
208           + ' - '
209           + this.completed + "/" + Tree.rowCount + " - "
210           + speed + '/s - DownThemAll!';
211       }
212       else if (this._running.length > 0) {
213         document.title =
214           Math.floor(this.completed * 100 / Tree.rowCount) + '%'
215           + ' - '       
216           + this.completed + "/" + Tree.rowCount + " - "
217           + speed + '/s - DownThemAll!';
218       }
219       else {
220         document.title = this.completed + "/" + Tree.rowCount + " - DownThemAll!";
221       }
222     }
223     catch(ex) {
224       Debug.log("refresh():", ex);
225     }
226   },
227   refreshWritten: function D_checkDownloads() {
228     this._running.forEach(
229       function(i) {
230         i.d.invalidate();
231       }
232     );
233   },
234   saveRunning: function D_saveRunning() {
235     if (!this._running.length) {
236       return;
237     }
238     SessionManager.beginUpdate();
239     this._running.forEach(
240       function(i) {
241         i.d.save();
242       }
243     );
244     SessionManager.endUpdate();
245   },
246
247   checkDownloads: function D_checkDownloads() {
248     try {
249       this.refresh();
250      
251       if (Prefs.autoClearComplete && this._autoClears.length) {
252         Tree.remove(this._autoClears);
253         this._autoClears = [];
254       }
255      
256       if (Prefs.autoRetryInterval) {
257         for (let d in Tree.all) {
258           d.autoRetry();
259         }
260       }
261          
262       this._running.forEach(
263         function(i) {
264           let d = i.d;
265           // checks for timeout
266           if (d.is(RUNNING) && (Utils.getTimestamp() - d.timeLastProgress) >= Prefs.timeout * 1000) {
267             if (d.resumable || !d.totalSize || !d.partialSize) {
268               d.pause();
269               d.markAutoRetry();
270               d.status = _("timeout");
271             }
272             else {
273               d.cancel(_("timeout"));
274             }
275             Debug.logString(d + " is a timeout");
276           }
277         }
278       )
279       this.startNext();
280     }
281     catch(ex) {
282       Debug.log("checkDownloads():", ex);
283     }
284   },
285   checkSameName: function D_checkSameName(download, path) {
286     for (let i = 0; i < this._running.length; ++i) {
287       if (this._running[i].d == download) {
288         continue;
289       }
290       if (this._running[i].d.destinationFile == path) {
291         return true;
292       }
293     }
294     return false;
295   },
296   startNext: function D_startNext() {
297     try {
298       var rv = false;
299       for (let d in Tree.all) {
300         if (this._running.length >= Prefs.maxInProgress) {
301           return rv;
302         }       
303         if (!d.is(QUEUED)) {
304           continue;
305         }
306         this.run(d);
307         rv = true;
308       }
309       return rv;
310     }
311     catch(ex){
312       Debug.log("startNext():", ex);
313     }
314     return false;
315   },
316   RunningJob: function(d) {
317     this.d = d;
318     this.lastBytes = d.partialSize;
319     this.lastTime = Utils.getTimestamp();
320   },
321   run: function D_run(download) {
322     download.status = _("starting");
323     if (download.is(FINISHING) || (download.partialSize >= download.totalSize && download.totalSize)) {
324       // we might encounter renaming issues;
325       // but we cannot handle it because we don't know at which stage we crashed
326       download.partialSize = download.totalSize;
327       Debug.logString("Download seems to be complete; likely a left-over from a crash, finish it:" + download);
328       download.finishDownload();
329       return;
330     }
331     download.timeLastProgress = Utils.getTimestamp();
332     download.timeStart = Utils.getTimestamp();
333     download.state = RUNNING;
334     if (!download.started) {
335       download.started = true;
336       Debug.logString("Let's start " + download);
337     }
338     else {
339       Debug.logString("Let's resume " + download + " at " + download.partialSize);
340     }
341     this._running.push(new Dialog.RunningJob(download));
342     download.resumeDownload();
343   },
344   wasStopped: function D_wasStopped(download) {
345     this._running = this._running.filter(
346       function(i) {
347         if (i.d == download) {
348           return false;
349         }
350         return true;
351       },
352       this
353     );
354   },
355   signal: function D_signal(download) {
356     download.save();
357     if (download.is(RUNNING)) {
358       this._wasRunning = true;
359     }
360     else if (Prefs.autoClearComplete && download.is(COMPLETE)) {
361       this._autoClears.push(download);
362     }
363     if (!this._initialized || !this._wasRunning || !download.is(COMPLETE)) {
364       return;
365     }
366     try {
367       // check if there is something running or scheduled
368       if (this.startNext() || Tree.some(function(d) { return d.is(FINISHING, RUNNING, QUEUED); } )) {
369         return;
370       }
371       Debug.logString("signal(): Queue finished");
372       Utils.playSound("done");
373      
374       let dp = Tree.at(0);
375       if (dp) {
376         dp = dp.destinationPath;
377       }
378       if (Prefs.alertingSystem == 1) {
379         AlertService.show(_("dcom"), _('suc'), dp, dp);
380       }
381       else if (dp && Prefs.alertingSystem == 0) {
382         if (confirm(_('suc') + "\n "+ _("folder")) == 1) {
383           try {
384             OpenExternal.launch(dp);
385           }
386           catch (ex){
387             // no-op
388           }
389         }
390       }
391       if (Prefs.autoClose) {
392         Dialog.close();
393       }
394     }
395     catch(ex) {
396       Debug.log("signal():", ex);
397     }
398   },
399   _canClose: function D__canClose() {
400     if (Tree.some(function(d) { return d.started && !d.resumable && d.is(RUNNING); })) {
401       var rv = DTA_confirmYN(
402         _("confclose"),
403         _("nonres")
404       );
405       if (rv) {
406         return false;
407       }
408     }
409     return (this._forceClose = true);
410   },
411   close: function D_close() {
412     Debug.logString("Close request");
413     if (!this._forceClose && !this._canClose()) {
414       delete this._forceClose;
415       return false;
416     }
417
418     // stop everything!
419     // enumerate everything we'll have to wait for!
420     if (this._updTimer) {
421       this._updTimer.kill();
422       delete this._updTimer;
423     }
424     let chunks = 0;
425     let finishing = 0;
426     Tree.updateAll(
427       function(d) {
428         if (d.is(RUNNING, QUEUED)) {
429           // enumerate all running chunks
430           d.chunks.forEach(
431             function(c) {
432               if (c.running) {
433                 ++chunks;
434               }
435             },
436             this
437           );
438           d.pause();       
439         }
440         else if (d.is(FINISHING)) {
441           ++finishing;
442         }
443       },
444       this
445     );
446     if (chunks || finishing) {
447       if (this._safeCloseAttempts < 20) {
448         ++this._safeCloseAttempts;
449         new Timer(function() { Dialog.close(); }, 250);       
450         return false;
451       }
452       Debug.logString("Going down even if queue was not probably closed yet!");
453     }
454     close();
455     return true;
456   },
457   _cleanTmpDir: function D__cleanTmpDir() {
458     if (!Prefs.tempLocation || Preferences.getMultiByteDTA("tempLocation", '') != '') {
459       // cannot perform this action if we don't use a temp file
460       // there might be far too many directories containing far too many tmpFiles.
461       // or part files from other users.
462       return;
463     }
464     let known = [];
465     for (d in Tree.all) {
466       known.push(d.tmpFile.leafName);
467     }
468     let tmpEnum = Prefs.tempLocation.directoryEntries;
469     let unknown = []
470     while (tmpEnum.hasMoreElements()) {
471       let f = tmpEnum.getNext().QueryInterface(Ci.nsILocalFile);
472       if (f.leafName.match(/\.dtapart$/) && known.indexOf(f.leafName) == -1) {
473         unknown.push(f);
474       }
475     }
476     unknown.forEach(
477       function(f) {
478         try {
479           f.remove(false);
480         }
481         catch(ex) {
482         }
483       }
484     );
485   },
486   _safeCloseAttempts: 0,
487
488   unload: function D_unload() {
489     TimerManager.killAll();
490     Prefs.shutdown();
491     try {
492       this._cleanTmpDir();
493     }
494     catch(ex) {
495       Debug.log("_safeClose", ex);
496     }
497     SessionManager.shutdown();
498     return true;   
499   }
500 };
501
502 function UrlManager(urls) {
503   this._urls = [];
504   this._idx = -1;
505
506   if (urls instanceof Array) {
507     this.initByArray(urls);
508     this._hasFresh = this._urls.length != 0;
509   }
510   else if (urls) {
511     throw "Feeding the UrlManager with some bad stuff is usually a bad idea!";
512   }
513 }
514 UrlManager.prototype = {
515   _sort: function(a,b) {
516     const rv = b.preference - a.preference;
517     return rv ? rv : (a.url < b.url ? -1 : 1);
518   },
519   initByArray: function um_initByArray(urls) {
520     for (let i = 0; i < urls.length; ++i) {
521       this.add(
522         new DTA_URL(
523           urls[i].url,
524           urls[i].charset,
525           urls[i].usable,
526           urls[i].preference
527         )
528       );
529     }
530     this._urls.sort(this._sort);
531     this._usable = this._urls[0].usable;
532   },
533   add: function um_add(url) {
534     if (!url instanceof DTA_URL) {
535       throw (url + " is not an DTA_URL");
536     }
537     if (!this._urls.some(function(ref) { return ref.url == url.url; })) {
538       this._urls.push(url);
539     }
540   },
541   getURL: function um_getURL(idx) {
542     if (typeof(idx) != 'number') {
543       this._idx++;
544       if (this._idx >= this._urls.length) {
545         this._idx = 0;
546       }
547       idx = this._idx;
548     }
549     return this._urls[idx];
550   },
551   get url() {
552     return this._urls[0].url;
553   },
554   get usable() {
555     return this._urls[0].usable;
556   },
557   get charset() {
558     return this._urls[0].charset;
559   },
560   get length() {
561     return this._urls.length;
562   },
563   get all() {
564     for (let i = 0, e = this._urls.length; i < e; ++i) {
565       yield this._urls[i];
566     }
567   },
568   markBad: function um_markBad(url) {
569     if (this._urls.length > 1) {
570       this._urls = this._urls.filter(function(u) { return u != url; });
571     }
572     else if (this._urls[0] == url) {
573       return false;
574     }
575     return true;
576   },
577   toSource: function um_toSource() {
578     let rv = [];
579     this._urls.forEach(
580       function(url) {
581         rv.push({
582           'url': url.url,
583           'charset': url.charset,
584           'usable': url.usable,
585           'preference': url.preference
586         });
587       }
588     );
589     return rv;
590   },
591   toString: function() {
592     let rv = '';
593     this._urls.forEach(
594       function(u) {
595         rv += u.preference + " " + u.url + "\n";
596       }
597     );
598     return rv;
599   }
600 };
601 function Visitor() {
602   // sanity check
603   if (arguments.length != 1) {
604     return;
605   }
606
607   var nodes = arguments[0];
608   for (x in nodes) {
609     if (!name || !(name in this.cmpKeys)) {
610       continue;
611     }
612     this[x] = nodes[x];
613   }
614 }
615
616 Visitor.prototype = {
617   cmpKeys: {
618     'etag': true, // must not be modified from 200 to 206: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
619     //'content-length': false,
620     'content-type': true,
621     'last-modified': true, // may get omitted later, but should not change
622     'content-encoding': true // must not change, or download will become corrupt.
623   },
624   type: null,
625   overrideCharset: null,
626   encoding: null,
627   fileName: null,
628   acceptRanges: 'bytes',
629   contentlength: 0,
630   time: null,
631
632   QueryInterface: function(aIID) {
633     if (
634       aIID.equals(Ci.nsISupports)
635       || aIID.equals(Ci.nsIHttpHeaderVisitor)
636     ) {
637       return this;
638     }
639     throw Components.results.NS_ERROR_NO_INTERFACE;
640   },
641   visitHeader: function(aHeader, aValue) {
642     try {
643       const header = aHeader.toLowerCase();
644       switch (header) {
645         case 'content-type': {
646           this.type = aValue;
647           var ch = aValue.match(/charset=['"]?([\w\d_-]+)/i);
648           if (ch && ch[1].length) {
649             DTA_debug.logString("visitHeader: found override to " + ch[1]);
650             this.overrideCharset = ch[1];
651           }
652         }
653         break;
654
655         case 'content-encoding':
656           this.encoding = aValue;
657         break;
658
659         case 'accept-ranges':
660           this.acceptRanges = aValue.toLowerCase().indexOf('none') == -1;
661           Debug.logString("acceptrange = " + aValue.toLowerCase());
662         break;
663
664         case 'content-length':
665           this.contentlength = Number(aValue);
666         break;
667
668         case 'content-range': {
669           let cl = new Number(aValue.split('/').pop());
670           if (cl > 0) {
671             this.contentlength = cl;
672           }
673         }
674         break;
675         case 'last-modified':
676           try {
677             this.time = Utils.getTimestamp(aValue);
678           }
679           catch (ex) {
680             Debug.log("gts", ex);
681           }
682         break;
683       }
684       if (header == 'etag') {
685         // strip off the "inode"-part apache and others produce, as mirrors/caches usually provide different/wrong numbers here :p
686         this[header] = aValue
687           .replace(/^(?:[Ww]\/)?"(.+)"$/, '$1')
688           .replace(/^[a-f\d]+-([a-f\d]+)-([a-f\d]+)$/, '$1-$2')
689           .replace(/^([a-f\d]+):[a-f\d]{1,6}$/, '$1');
690           Debug.logString("Etag: " + this[header] + " - " + aValue);
691       }
692       else if (header in this.cmpKeys) {
693         this[header] = aValue;
694       }
695       if ((header == 'content-type' || header == 'content-disposition') && this.fileName == null) {
696         // we have to handle headers like "content-disposition: inline; filename='dummy.txt'; title='dummy.txt';"
697         var value = aValue.match(/file(?:name)?=(["']?)([^\1;]+)\1(?:;.+)?/i);
698         if (!value) {
699           // workaround for bug #13959
700           // attachments on some vbulletin forums send nasty headers like "content-disposition: inline; filename*=utf-8''file.ext"
701           value = aValue.match(/file(?:name)?\*=(.*)''(.+)/i);
702           if (value) {
703             this.overrideCharset = value[1];
704           }
705         }
706         if (value) {
707           this.fileName = value[2].getUsableFileName();
708         }
709       }
710     }
711     catch (ex) {
712       Debug.log("hrhv::visitHeader:", ex);
713     }
714   },
715   compare: function vi_compare(v) {
716     if (!(v instanceof Visitor)) {
717       return;
718     }
719
720     for (x in this.cmpKeys) {
721       // we don't have this header
722       if (!(x in this)) {
723         continue;
724       }
725       // v does not have this header
726       else if (!(x in v)) {
727         // allowed to be missing?
728         if (this.cmpKeys[x]) {
729           continue;
730         }
731         Debug.logString(x + " missing");
732         throw new Exception(x + " is missing");
733       }
734       // header is there, but differs
735       else if (this[x] != v[x]) {
736         Debug.logString(x + " nm: [" + this[x] + "] [" + v[x] + "]");
737         throw new Exception("Header " + x + " doesn't match");
738       }
739     }
740   },
741   save: function vi_save(node) {
742     var rv = {};
743     // salva su file le informazioni sugli headers
744     for (x in this.cmpKeys) {
745       if (!(x in this)) {
746         continue;
747       }
748       rv[x] = this[x];
749     }
750     return rv;
751   }
752 };
753
754 /**
755  * Visitor Manager c'tor
756  * @author Nils
757  */
758 function VisitorManager(nodes) {
759   this._visitors = {};
760   if (nodes) {
761     this._load(nodes);
762   }
763 }
764 VisitorManager.prototype = {
765   /**
766    * Loads a ::save'd JS Array
767    * Will silently bypass failed items!
768    * @author Nils
769    */
770   _load: function vm_init(nodes) {
771     for (let i = 0; i < nodes.length; ++i) {
772       try {
773         this._visitors[nodes[i].url] = new Visitor(nodes[i].values);
774       }
775       catch (ex) {
776         Debug.log("failed to read one visitor", ex);
777       }
778     }
779   },
780   /**
781    * Saves/serializes the Manager and associated Visitors to an JS Array
782    * @return A ::load compatible Array
783    * @author Nils
784    */
785   toSource: function vm_toSource() {
786     var rv = [];
787     for (let x in this._visitors) {
788       try {
789         var v = {};
790         v.url = x;
791         v.values = this._visitors[x].save();
792         rv.push(v);
793       }
794       catch(ex) {
795         Debug.log(x, ex);
796       }
797     }
798     return rv;
799   },
800   /**
801    * Visit and compare a channel
802    * @returns visitor for channel
803    * @throws Exception if comparision yield a difference (i.e. channels are not "compatible")
804    * @author Nils
805    */
806   visit: function vm_visit(chan) {
807     var url = chan.URI.spec;
808
809     var visitor = new Visitor();
810     chan.visitResponseHeaders(visitor);
811     if (url in this._visitors)
812     {
813         this._visitors[url].compare(visitor);
814     }
815     return (this._visitors[url] = visitor);
816   },
817   /**
818    * return the first timestamp registered with a visitor
819    * @throws Exception if no timestamp found
820    * @author Nils
821    */
822   get time() {
823     for (let i in this._visitors) {
824       if (this._visitors[i].time > 0) {
825         return this._visitors[i].time;
826       }
827     }
828     throw new Exception("No Date registered");
829   }
830 };
831
832 function QueueItem(lnk, dir, num, desc, mask, referrer, tmpFile) {
833
834   this.visitors = new VisitorManager();
835
836   this.startDate = new Date(); 
837
838   this.chunks = [];
839   this.speeds = new Array();
840  
841 }
842
843 QueueItem.prototype = {
844   _state: QUEUED,
845   get state() {
846     return this._state;
847   },
848   set state(nv) {
849     if (this._state != nv) {
850       if (this._state == RUNNING) {
851         // remove ourself from inprogresslist
852         Dialog.wasStopped(this);
853       }
854       this._state = nv;
855       this.invalidate();
856       Tree.refreshTools();
857       Dialog.signal(this);
858     }
859   },
860  
861   postData: null,
862  
863   _fileName: null,
864   get fileName() {
865     return this._fileName;
866   },
867   set fileName(nv) {
868     this._fileName = nv;
869     this.rebuildDestination();
870     this.invalidate();
871     return nv;
872   },
873   _description: null,
874   get description() {
875     return this._description;
876   },
877   set description(nv) {
878     this._description = nv;
879     this.rebuildDestination();
880     this.invalidate();
881     return nv;
882   }, 
883
884   _pathName: null,
885   get pathName() {
886     return this._pathName;
887   },
888   set pathName(nv) {
889     this._pathName = nv;
890     this.rebuildDestination();
891     this.invalidate();
892     return nv;
893   }, 
894
895   _mask: null,
896   get mask() {
897     return this._mask;
898   },
899   set mask(nv) {
900     this._mask = nv;
901     this.rebuildDestination();
902     this.invalidate();
903     return nv;
904   },   
905  
906   _destinationName: null,
907   destinationNameOverride: null,
908   _destinationNameFull: null,
909   get destinationName() {
910     return this._destinationNameFull;
911   },
912   set destinationName(nv) {
913     this.destinationNameOverride = nv;
914     this.rebuildDestination();
915     this.invalidate();
916     return this._destinationNameFull;
917   },
918  
919   _destinationFile: null,
920   get destinationFile() {
921     if (!this._destinationFile) {
922       this.rebuildDestination();
923     }
924     return this._destinationFile;
925   },
926  
927   _conflicts: 0,
928   get conflicts() {
929     return this._conflicts;
930   },
931   set conflicts(nv) {
932     if (typeof(nv) != 'number') {
933       return this._conflicts;
934     }
935     this._conflicts = nv;
936     this.rebuildDestination();
937     this.invalidate();
938     return nv;
939   },
940   _tmpFile: null,
941   get tmpFile() {
942     if (!this._tmpFile) {
943       var dest = Prefs.tempLocation
944         ? Prefs.tempLocation.clone()
945         : new FileFactory(this.destinationPath);
946       let name = this.fileName;
947       if (name.length > 60) {
948         name = name.substring(0, 60);
949       }
950       dest.append(name + "-" + newUUIDString() + '.dtapart');
951       this._tmpFile = dest;
952     }
953     return this._tmpFile;
954   },
955   _hash: null,
956   get hash() {
957     return this._hash;
958   },
959   set hash(nv) {
960     this._hash = nv;
961     this._prettyHash = this.hash ? _('prettyhash', [this.hash.type, this.hash.sum]) : _('nas');
962   },
963   _prettyHash: null,
964   get prettyHash() {
965     return this._prettyHash;
966   },
967
968   /**
969    *Takes one or more state indicators and returns if this download is in state of any of them
970    */
971   is: function QI_is() {
972     let state = this.state;
973     for (let i = 0, e = arguments.length; i < e; ++i) {
974       if (state == arguments[i]) {
975         return true;
976       }
977     }
978     return false;
979   },
980  
981   save: function QI_save() {
982     if (this.dbId) {
983       if (
984         (Prefs.removeCompleted && this.is(COMPLETE))
985         || (Prefs.removeCanceled && this.is(CANCELED))
986         || (Prefs.removeAborted && this.is(PAUSED))
987       ) {
988         this.remove();
989         return false;
990       }
991       SessionManager.saveDownload(this.dbId, this.toSource());
992       return true;
993     }
994
995     this.dbId = SessionManager.addDownload(this.toSource());
996     return true;
997   },
998   remove: function QI_remove() {
999     SessionManager.deleteDownload(this.dbId);
1000     delete this.dbId;
1001     this.position = -1;
1002   },
1003   _position: -1,
1004   get position() {
1005     return this._position;
1006   },
1007   set position(nv) {
1008     if (nv == this._position) {
1009       return;
1010     }
1011     this._position = nv;
1012     if (this.dbId && this._position != -1) {
1013       SessionManager.savePosition(this.dbId, this._position);
1014     }
1015   },
1016
1017   contentType: "",
1018   visitors: null,
1019   _totalSize: 0,
1020   get totalSize() { return this._totalSize; },
1021   set totalSize(nv) {
1022     this._totalSize = nv;
1023     this.invalidate();
1024     return this._totalSize;
1025   },
1026   partialSize: 0,
1027
1028   startDate: null,
1029
1030   compression: null,
1031
1032   resumable: true,
1033   started: false,
1034
1035   _activeChunks: 0,
1036   get activeChunks() {
1037     return this._activeChunks;
1038   },
1039   set activeChunks(nv) {
1040     nv = Math.max(0, nv);
1041     this._activeChunks = nv;
1042     this.invalidate();
1043     return this._activeChunks;
1044   },
1045   _maxChunks: 0,
1046   get maxChunks() {
1047     if (!this._maxChunks) {
1048         this._maxChunks = Prefs.maxChunks;
1049     }
1050     return this._maxChunks;
1051   },
1052   set maxChunks(nv) {
1053     this._maxChunks = nv;
1054     if (this._maxChunks < this._activeChunks) {
1055       let running = this.chunks.filter(function(c) { return c.running; });
1056       while (running.length && this._maxChunks < running.length) {
1057         let c = running.pop();
1058         if (c.remainder < 10240) {
1059           continue;
1060         }
1061         c.cancel();
1062       }
1063     }
1064     else if (this._maxChunks > this._activeChunks && this.is(RUNNING)) {
1065       this.resumeDownload();
1066      
1067     }
1068     this.invalidate();
1069     Debug.logString("mc set to " + nv);
1070     return this._maxChunks;
1071   },
1072   timeLastProgress: 0,
1073   timeStart: 0,
1074
1075   _icon: null,
1076   get icon() {
1077     if (!this._icon) {
1078       this._icon = getIcon(this.destinationName, 'metalink' in this);
1079     }
1080     return this._icon;
1081   },
1082   get largeIcon() {
1083     return getIcon(this.destinationName, 'metalink' in this, 32);
1084   },
1085   get size() {
1086     try {
1087       let file = new FileFactory(this.destinationFile);
1088       if (file.exists()) {
1089         return file.fileSize;
1090       }
1091     }
1092     catch (ex) {
1093       Debug.log("download::getSize(): ", e)
1094     }
1095     return 0;
1096   },
1097   get dimensionString() {
1098     if (this.partialSize <= 0) {
1099       return _('unknown');
1100     }
1101     else if (this.totalSize <= 0) {
1102       return _('transfered', [Utils.formatBytes(this.partialSize), _('nas')]);
1103     }
1104     else if (this.is(COMPLETE)) {
1105       return Utils.formatBytes(this.totalSize);
1106     }
1107     return _('transfered', [Utils.formatBytes(this.partialSize), Utils.formatBytes(this.totalSize)]);
1108   },
1109   _status : '',
1110   get status() {
1111     return this._status + (this._autoRetryTime ? ' *' : '');
1112   },
1113   set status(nv) {
1114     if (nv != this._status) {
1115       this._status = nv;
1116       this.invalidate();
1117     }
1118     return this._status;
1119   },
1120   get parts() {
1121     if (this.maxChunks) {
1122       return (this.activeChunks) + '/' + this.maxChunks;
1123     }
1124     return '';
1125   },
1126   get percent() {
1127     if (!this.totalSize && this.is(RUNNING)) {
1128       return _('nas');
1129     }
1130     else if (!this.totalSize) {
1131       return "0%";
1132     }
1133     else if (this.is(COMPLETE)) {
1134       return "100%";
1135     }
1136     return Math.floor(this.partialSize / this.totalSize * 100) + "%";
1137   },
1138   _destinationPath: '',
1139   get destinationPath() {
1140     return this._destinationPath;
1141   },
1142
1143   invalidate: function QI_invalidate() {
1144     Tree.invalidate(this);
1145   },
1146
1147   safeRetry: function QI_safeRetry() {
1148     // reset flags
1149     this.totalSize = this.partialSize = 0;
1150     this.compression = null;
1151     this.activeChunks = this.maxChunks = 0;
1152     this.chunks.forEach(function(c) { c.cancel(); });
1153     this.chunks = [];
1154     this.speeds = [];
1155     this.visitors = new VisitorManager();
1156     Dialog.run(this);
1157   },
1158
1159   refreshPartialSize: function QI_refreshPartialSize(){
1160     var size = 0;
1161     this.chunks.forEach(function(c) { size += c.written; });
1162     this.partialSize = size;
1163   },
1164
1165   pause: function QI_pause(){
1166     if (this.chunks) {
1167       for (let i = 0, e = this.chunks.length; i < e; ++i) {
1168         if (this.chunks[i].running) {
1169           this.chunks[i].cancel();
1170         }
1171       }
1172     }
1173     this.activeChunks = 0;
1174     this.state = PAUSED;
1175     this.speeds = [];
1176   },
1177
1178   moveCompleted: function QI_moveCompleted() {
1179     if (this.is(CANCELED)) {
1180       return;
1181     }
1182     ConflictManager.resolve(this, 'continueMoveCompleted');
1183   },
1184   continueMoveCompleted: function QI_continueMoveCompleted() {
1185     if (this.is(CANCELED)) {
1186       return;
1187     }   
1188     try {
1189       // safeguard against some failed chunks.
1190       this.chunks.forEach(function(c) { c.close(); });
1191       var destination = new FileFactory(this.destinationPath);
1192       Debug.logString(this.fileName + ": Move " + this.tmpFile.path + " to " + this.destinationFile);
1193
1194       if (!destination.exists()) {
1195         destination.create(Ci.nsIFile.DIRECTORY_TYPE, Prefs.dirPermissions);
1196         this.invalidate();
1197       }
1198       var df = destination.clone();
1199       df.append(this.destinationName);
1200       if (df.exists()) {
1201         df.remove(false);
1202       }
1203       // move file
1204       if (this.compression) {
1205         DTA_include("dta/manager/decompressor.js");
1206         new Decompressor(this);
1207       }
1208       else {
1209         this.tmpFile.clone().moveTo(destination, this.destinationName);
1210         this.complete();
1211       }
1212     }
1213     catch(ex) {
1214       Debug.log("continueMoveCompleted encountered an error", ex);
1215       this.complete(ex);
1216     }
1217   },
1218   handleMetalink: function QI_handleMetaLink() {
1219     try {
1220       DTA_include("dta/manager/metalinker.js");
1221       Metalinker.handleDownload(this);
1222     }
1223     catch (ex) {
1224       Debug.log("handleMetalink", ex);
1225     }
1226   },
1227   verifyHash: function() {
1228     DTA_include("dta/manager/verificator.js");
1229     new Verificator(this);
1230   },
1231   customFinishEvent: function() {
1232     DTA_include("dta/manager/customevent.js");
1233     new CustomEvent(this, Prefs.finishEvent);
1234   },
1235   setAttributes: function() {
1236     if (Prefs.setTime) {
1237       try {
1238         var time = this.startDate.getTime();
1239         try {
1240           var time =  this.visitors.time;
1241         }
1242         catch (ex) {
1243           // no-op
1244         }
1245         // small validation. Around epoche? More than a month in future?
1246         if (time < 2 || time > Date.now() + 30 * 86400000) {
1247           throw new Exception("invalid date encountered: " + time + ", will not set it");
1248         }
1249         // have to unwrap
1250         var file = new FileFactory(this.destinationFile);
1251         file.lastModifiedTime = time;
1252       }
1253       catch (ex) {
1254         Debug.log("Setting timestamp on file failed: ", ex);
1255       }
1256     }
1257     this.totalSize = this.partialSize = this.size;
1258     ++Dialog.completed;
1259    
1260     this.complete();
1261   },
1262   finishDownload: function QI_finishDownload(exception) {
1263     Debug.logString("finishDownload, connections: " + this.sessionConnections);
1264     this._completeEvents = ['moveCompleted', 'setAttributes'];
1265     if (this.hash) {
1266       this._completeEvents.push('verifyHash');
1267     }
1268     if ('isMetalink' in this) {
1269       this._completeEvents.push('handleMetalink');
1270     }
1271     if (Prefs.finishEvent) {
1272       this._completeEvents.push('customFinishEvent');
1273     }
1274     this.complete();
1275   },
1276   _completeEvents: [],
1277   complete: function QI_complete(exception) {
1278     if (exception) {
1279       this.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
1280       Debug.log("complete: ", exception);
1281       return;
1282     }
1283     if (this._completeEvents.length) {
1284       var evt = this._completeEvents.shift();
1285       var tp = this;
1286       window.setTimeout(
1287         function() {
1288           try {
1289             tp[evt]();
1290           }
1291           catch(ex) {
1292             Debug.log("completeEvent failed: " + evt, ex);
1293             tp.complete();
1294           }
1295         },
1296         0
1297       );
1298       return;
1299     }
1300     this.chunks = [];   
1301     this.activeChunks = 0;
1302     this.state = COMPLETE;
1303     this.status = _("complete");
1304   },
1305   rebuildDestination: function QI_rebuildDestination() {
1306     try {
1307       let uri = this.urlManager.usable.toURL();
1308       let host = uri.host.toString();
1309
1310       // normalize slashes
1311       let mask = this.mask
1312         .normalizeSlashes()
1313         .removeLeadingSlash()
1314         .removeFinalSlash();
1315
1316       let uripath = uri.path.removeLeadingChar("/");
1317       if (uripath.length) {
1318         uripath = uripath.substring(0, uri.path.lastIndexOf("/"))
1319           .normalizeSlashes()
1320           .removeFinalSlash();
1321       }
1322
1323       let query = '';
1324       try {
1325         query = uri.query;
1326       }
1327       catch (ex) {
1328         // no-op
1329       }
1330
1331       let description = this.description.removeBadChars().replaceSlashes(' ').trim();
1332      
1333       let name = this.fileName;
1334       let ext = name.getExtension();
1335       if (ext) {
1336         name = name.substring(0, name.length - ext.length - 1);
1337
1338         if (this.contentType && /htm/.test(this.contentType) && !/htm/.test(ext)) {
1339           ext += ".html";
1340         }
1341       }
1342       // mime-service method
1343       else if (this.contentType && /^(?:image|text)/.test(this.contentType)) {
1344         try {
1345           let info = MimeService.getFromTypeAndExtension(this.contentType.split(';')[0], "");
1346           ext = info.primaryExtension;
1347         } catch (ex) {
1348           ext = '';
1349         }
1350       }
1351       else {
1352         name = this.fileName;
1353         ext = '';
1354       }
1355       let ref = this.referrer ? this.referrer.host.toString() : '';
1356      
1357       let curl = (uri.host + ((uripath=="") ? "" : (SYSTEMSLASH + uripath)));
1358      
1359       var replacements = {
1360         "name": name,
1361         "ext": ext,
1362         "text": description,
1363         "url": host,
1364         "subdirs": uripath,
1365         "flatsubdirs": uripath.replaceSlashes('-'),
1366         "refer": ref,
1367         "qstring": query,
1368         "curl": curl,
1369         "flatcurl": curl.replaceSlashes('-'),
1370         "num": Utils.formatNumber(this.numIstance),
1371         "hh": Utils.formatNumber(this.startDate.getHours(), 2),
1372         "mm": Utils.formatNumber(this.startDate.getMinutes(), 2),
1373         "ss": Utils.formatNumber(this.startDate.getSeconds(), 2),
1374         "d": Utils.formatNumber(this.startDate.getDate(), 2),
1375         "m": Utils.formatNumber(this.startDate.getMonth() + 1, 2),
1376         "y": String(this.startDate.getFullYear())
1377       }
1378       function replacer(type) {
1379         var t = type.substr(1, type.length - 2);
1380         if (t in replacements) {
1381           return replacements[t];
1382         }
1383         return type;
1384       }
1385      
1386       mask = mask.replace(/\*\w+\*/gi, replacer);
1387
1388       mask = mask.removeBadChars().removeFinalChar(".").trim().split(SYSTEMSLASH);
1389       let file = new FileFactory(this.pathName.addFinalSlash());
1390       while (mask.length) {
1391         file.append(mask.shift());
1392       }
1393       this._destinationName = file.leafName;
1394       this._destinationPath = file.parent.path;
1395     }
1396     catch(ex) {
1397       this._destinationName = this.fileName;
1398       this._destinationPath = this.pathName.addFinalSlash();
1399       Debug.log("rebuildDestination():", ex);
1400     }
1401     this._destinationNameFull = Utils.formatConflictName(
1402       this.destinationNameOverride ? this.destinationNameOverride : this._destinationName,
1403       this.conflicts
1404     );
1405     let file = new FileFactory(this.destinationPath);
1406     file.append(this.destinationName);
1407     this._destinationFile = file.path;
1408     this._icon = null;
1409   },
1410
1411   fail: function QI_fail(title, msg, state) {
1412     Debug.logString("failDownload invoked");
1413
1414     this.cancel(state);
1415
1416     Utils.playSound("error");
1417
1418     switch (Prefs.alertingSystem) {
1419       case 1:
1420         AlertService.show(title, msg, false);
1421         break;
1422       case 0:
1423         alert(msg);
1424         break;
1425     }
1426   },
1427
1428   cancel: function QI_cancel(message) {
1429     try {
1430       if (this.is(CANCELED)) {
1431         return;
1432       }
1433       if (this.is(COMPLETE)) {
1434         Dialog.completed--;
1435       }
1436       else if (this.is(RUNNING)) {
1437         this.pause();
1438       }
1439       this.state = CANCELED;     
1440       Debug.logString(this.fileName + ": canceled");
1441
1442       this.visitors = new VisitorManager();
1443
1444       if (message == "" || !message) {
1445         message = _("canceled");
1446       }
1447       this.status = message;
1448
1449
1450       this.removeTmpFile();
1451
1452       // gc
1453       this.chunks = [];
1454       this.totalSize = this.partialSize = 0;
1455       this.maxChunks = this.activeChunks = 0;
1456       this.conflicts = 0;
1457       this.resumable = true;
1458       this._autoRetries = 0;
1459
1460     } catch(ex) {
1461       Debug.log("cancel():", ex);
1462     }
1463   },
1464  
1465   removeTmpFile: function QI_removeTmpFile() {
1466     if (this.tmpFile.exists()) {
1467       try {
1468         this.tmpFile.remove(false);
1469       }
1470       catch (ex) {
1471         Debug.log("failed to remove tmpfile: " + this.tmpFile.path, ex);
1472       }
1473     }
1474     else {
1475       Debug.logString("tmpfile not found: " + this.tmpFile.path);
1476     }
1477   },
1478   sessionConnections: 0,
1479   _autoRetries: 0,
1480   _autoRetryTime: 0,
1481   markAutoRetry: function QI_markRetry() {
1482     if (!Prefs.autoRetryInterval || (Prefs.maxAutoRetries && Prefs.maxAutoRetries <= this._autoRetries)) {
1483        return;
1484     }
1485     this._autoRetryTime = Utils.getTimestamp();
1486     Debug.logString("marked auto-retry: " + d);
1487   },
1488   autoRetry: function QI_autoRetry() {
1489     if (!this._autoRetryTime || Utils.getTimestamp() - (Prefs.autoRetryInterval * 1000) < this._autoRetryTime) {
1490       return;
1491     }
1492
1493     this._autoRetryTime = 0;
1494     ++this._autoRetries;
1495     this.queue();
1496     Debug.logString("Requeued due to auto-retry: " + d);   
1497   },
1498   queue: function QI_queue() {
1499     this._autoRetryTime = 0;
1500     this.state = QUEUED;
1501     this.status = _("inqueue");
1502   },
1503   resumeDownload: function QI_resumeDownload() {
1504     Debug.logString("resumeDownload: " + this);
1505     function cleanChunks(d) {
1506       // merge finished chunks together, so that the scoreboard does not bloat that much
1507       for (let i = d.chunks.length - 2; i > -1; --i) {
1508         let c1 = d.chunks[i], c2 = d.chunks[i + 1];
1509         if (c1.complete && c2.complete) {
1510           c1.merge(c2);
1511           d.chunks.splice(i + 1, 1);
1512         }
1513       }
1514     }
1515     function downloadNewChunk(download, start, end, header) {
1516       var chunk = new Chunk(download, start, end);
1517       Debug.logString("started: " + chunk);
1518       download.chunks.push(chunk);
1519       download.chunks.sort(function(a,b) { return a.start - b.start; });
1520       downloadChunk(download, chunk, header);
1521       download.sessionConnctions = 0;
1522     }
1523     function downloadChunk(download, chunk, header) {
1524       chunk.running = true;
1525       download.state = RUNNING;
1526       Debug.logString("started: " + chunk);
1527       chunk.download = new Connection(download, chunk, header);
1528       ++download.activeChunks;
1529       ++download.sessionConnections;
1530     }
1531    
1532     cleanChunks(this);
1533
1534     try {
1535       if (this.maxChunks <= this.activeChunks) {
1536         return false;
1537       }
1538
1539       var rv = false;
1540
1541       // we didn't load up anything so let's start the main chunk (which will grab the info)
1542       if (this.chunks.length == 0) {
1543         downloadNewChunk(this, 0, 0, true);
1544         return false;
1545       }
1546
1547       // start some new chunks
1548       var paused = this.chunks.filter(
1549         function (chunk) {
1550           return !(chunk.running || chunk.complete);
1551         }
1552       );
1553       while (this.activeChunks < this.maxChunks) {
1554
1555         // restart paused chunks
1556         if (paused.length) {
1557           downloadChunk(this, paused.shift());
1558           rv = true;
1559           continue;
1560         }
1561        
1562         // find biggest chunk
1563         let biggest = null;
1564         this.chunks.forEach(
1565           function (chunk) {
1566             if (chunk.running && chunk.remainder > MIN_CHUNK_SIZE * 2) {
1567               if (!biggest || biggest.remainder < chunk.remainder) {
1568                 biggest = chunk;
1569               }
1570             }
1571           }
1572         );
1573
1574         // nothing found, break
1575         if (!biggest) {
1576           break;
1577         }
1578         var end = biggest.end;
1579         var bend = biggest.start + biggest.written + Math.floor(biggest.remainder / 2);
1580         biggest.end = bend;
1581         downloadNewChunk(this, biggest.end + 1, end);
1582         rv = true;
1583       }
1584
1585       return rv;
1586     }
1587     catch(ex) {
1588       Debug.log("resumeDownload():", ex);
1589     }
1590     return false;
1591   },
1592   dumpScoreboard: function QI_dumpScoreboard() {
1593     let scoreboard = '';
1594     let len = String(this.totalSize).length;
1595     this.chunks.forEach(
1596       function(c,i) {
1597         scoreboard += i
1598           + ": "
1599           + c
1600           + "\n";
1601       }
1602     );
1603     Debug.logString("scoreboard\n" + scoreboard);
1604   }, 
1605   toString: function() {
1606     return this.urlManager.usable;
1607   },
1608   toSource: function() {
1609     let e = {};
1610     [
1611       'fileName',
1612       'postData',
1613       'numIstance',
1614       'description',
1615       'resumable',
1616       'mask',
1617       'pathName',
1618       'compression',
1619       'maxChunks',
1620       'contentType',
1621       'conflicts',
1622     ].forEach(
1623       function(u) {
1624         e[u] = this[u];
1625       },
1626       this
1627     );
1628     if (this.hash) {
1629       e.hash = _atos(this.hash.sum);
1630       e.hashType = _atos(this.hash.type);
1631     }
1632     e.state = this.is(COMPLETE, CANCELED, FINISHING) ? this.state : PAUSED;
1633     if (this.destinationNameOverride) {
1634       this.destinationName = this.destinationNameOverride;
1635     }
1636     if (this.referrer) {
1637       e.referrer = this.referrer.spec;
1638     }
1639     // Store this so we can later resume.
1640     if (!this.is(CANCELED, COMPLETE) && this.partialSize) {
1641       e.tmpFile = this.tmpFile.path;
1642     }
1643     e.startDate = this.startDate.getTime();
1644
1645     e.urlManager = this.urlManager.toSource();
1646     e.visitors = this.visitors.toSource();
1647
1648     if (!this.resumable && !this.is(COMPLETE)) {
1649       e.totalSize = 0;
1650     }
1651     else {
1652       e.totalSize = this.totalSize;
1653     }
1654    
1655     e.chunks = [];
1656
1657     if (this.is(RUNNING, PAUSED, QUEUED) && this.resumable) {
1658       this.chunks.forEach(
1659         function(c) {
1660           e.chunks.push({start: c.start, end: c.end, written: c.safeBytes});
1661         }
1662       );
1663     }
1664     return e.toSource();
1665   }
1666 }
1667
1668 function Chunk(download, start, end, written) {
1669   // saveguard against null or strings and such
1670   this._written = written > 0 ? written : 0;
1671   this._buffered = 0;
1672   this._start = start;
1673   this._end = end;
1674   this.end = end;
1675   this._parent = download;
1676   this._sessionbytes = 0;
1677 }
1678
1679 Chunk.prototype = {
1680   running: false,
1681   get starter() {
1682     return this.end <= 0;
1683   },
1684   get start() {
1685     return this._start;
1686   },
1687   get end() {
1688     return this._end;
1689   },
1690   set end(nv) {
1691     this._end = nv;
1692     this._total = this._end - this._start + 1;
1693   },
1694   get total() {
1695     return this._total;
1696   },
1697   get written() {
1698     return this._written;
1699   },
1700   get safeBytes() {
1701     return this.written - this._buffered;
1702   },
1703   get remainder() {
1704     return this._total - this._written;
1705   },
1706   get complete() {
1707     if (this._end == -1) {
1708       return this.written != 0;
1709     }
1710     return this._total == this.written;
1711   },
1712   get parent() {
1713     return this._parent;
1714   },
1715   merge: function CH_merge(ch) {
1716     if (!this.complete && !ch.complete) {
1717       throw new Error("Cannot merge incomplete chunks this way!");
1718     }
1719     this.end = ch.end;
1720     this._written += ch._written;
1721   },
1722   open: function CH_open() {
1723     this._sessionBytes = 0;
1724     let file = this.parent.tmpFile.clone();
1725     if (!file.parent.exists()) {
1726       file.parent.create(Ci.nsIFile.DIRECTORY_TYPE, Prefs.dirPermissions);
1727       this.parent.invalidate();
1728     }
1729     let prealloc = !file.exists();
1730     if (prealloc && this.parent.totalSize > 0) {
1731       try {
1732         file.create(file.NORMAL_FILE_TYPE, Prefs.permissions);
1733         file.fileSize = this.parent.totalSize;
1734         Debug.logString("fileSize set using #1");
1735         prealloc = false;
1736       }
1737       catch (ex) {
1738         // no op
1739       }
1740     }   
1741     let outStream = new FileOutputStream(file, 0x02 | 0x08, Prefs.permissions, 0);
1742     let seekable = outStream.QueryInterface(Ci.nsISeekableStream);
1743     if (prealloc && this.parent.totalSize > 0) {
1744       try {
1745         seekable.seek(0x00, this.parent.totalSize);
1746         seekable.setEOF();
1747         Debug.logString("fileSize set using #2");
1748       }
1749       catch (ex) {
1750         // no-op
1751       }
1752     }
1753     seekable.seek(0x00, this.start + this.written);
1754     this._outStream = new BufferedOutputStream(outStream, CHUNK_BUFFER_SIZE);
1755   },
1756   close: function CH_close() {
1757     this.running = false;
1758     if (this._outStream) {
1759       this._outStream.flush();
1760       this._outStream.close();
1761       delete this._outStream;
1762     }
1763     this._buffered = 0;
1764     if (this.parent.is(CANCELED)) {
1765       this.parent.removeTmpFile();
1766     }
1767   },
1768   rollback: function CH_rollback() {
1769     if (!this._sessionBytes || this._sessionBytes > this._written) {
1770       return;
1771     }
1772     this._written -= this._sessionBytes;
1773     this._sessionBytes = 0;
1774   },
1775   cancel: function CH_cancel() {
1776     this.running = false;
1777     this.close();
1778     if (this.download) {
1779       this.download.cancel();
1780     }
1781   },
1782   _written: 0,
1783   _outStream: null,
1784   write: function CH_write(aInputStream, aCount) {
1785     try {
1786       if (!this._outStream) {
1787         this.open();
1788       }
1789       bytes = this.remainder;
1790       if (!this.total || aCount < bytes) {
1791         bytes = aCount;
1792       }
1793       if (!bytes) {
1794         return 0;
1795       }
1796       if (bytes < 0) {
1797         throw new Exception("bytes negative");
1798       }
1799       // we're using nsIFileOutputStream
1800       if (this._outStream.writeFrom(aInputStream, bytes) != bytes) {
1801         throw ("chunks::write: read/write count mismatch!");
1802       }
1803       this._written += bytes;
1804       this._sessionBytes += bytes;
1805       this._buffered = Math.min(CHUNK_BUFFER_SIZE, this._buffered + bytes);
1806
1807       this.parent.timeLastProgress = Utils.getTimestamp();
1808
1809       return bytes;
1810     }
1811     catch (ex) {
1812       Debug.log('write: ' + this.parent.tmpFile.path, ex);
1813       throw ex;
1814     }
1815     return 0;
1816   },
1817   toString: function() {
1818     let len = this.parent.totalSize ? String(this.parent.totalSize).length  : 10;
1819     return Utils.formatNumber(this.start, len)
1820       + "/"
1821       + Utils.formatNumber(this.end, len)
1822       + "/"
1823       + Utils.formatNumber(this.total, len)
1824       + " running:"
1825       + this.running
1826       + " written/remain:"
1827       + Utils.formatNumber(this.written, len)
1828       + "/"
1829       + Utils.formatNumber(this.remainder, len);
1830   }
1831 }
1832
1833 var Prompts = {
1834   _authPrompter: null,
1835   _prompter: null,
1836   get authPrompter() {
1837     if (!this._authPrompter) {
1838       this._authPrompter = WindowWatcherService.getNewAuthPrompter(window)
1839         .QueryInterface(Ci.nsIAuthPrompt);   
1840     }
1841     return this._authPrompter;
1842   },
1843   get prompter() {
1844     if (!this._prompter) {
1845       this._prompter = WindowWatcherService.getNewPrompter(window)
1846         .QueryInterface(Ci.nsIPrompt);
1847     }
1848     return this._prompter;
1849   }
1850 };
1851
1852 function Connection(d, c, getInfo) {
1853
1854   this.d = d;
1855   this.c = c;
1856   this.isInfoGetter = getInfo;
1857   this.url = d.urlManager.getURL();
1858   var referrer = d.referrer;
1859   Debug.logString("starting: " + this.url.url);
1860
1861   this._chan = IOService.newChannelFromURI(this.url.url.toURL());
1862   var r = Ci.nsIRequest;
1863   this._chan.loadFlags = r.LOAD_NORMAL | r.LOAD_BYPASS_CACHE;
1864   this._chan.notificationCallbacks = this;
1865   try {
1866     var encodedChannel = this._chan.QueryInterface(Ci.nsIEncodedChannel);
1867     encodedChannel.applyConversion = false;
1868   }
1869   catch (ex) {
1870     // no-op
1871   }
1872   try {
1873     let http = this._chan.QueryInterface(Ci.nsIHttpChannel);
1874     if (c.start + c.written > 0) {
1875       http.setRequestHeader('Range', 'bytes=' + (c.start + c.written) + "-", false);
1876     }
1877     if (referrer instanceof Ci.nsIURI) {
1878       http.referrer = referrer;
1879     }
1880     http.setRequestHeader('Keep-Alive', '', false);
1881     http.setRequestHeader('Connection', 'close', false);
1882     if (d.postData) {
1883       let uc = http.QueryInterface(Ci.nsIUploadChannel);
1884       uc.setUploadStream(new StringInputStream(d.postData, d.postData.length), null, -1);
1885       http.requestMethod = 'POST';
1886     }     
1887   }
1888   catch (ex) {
1889     Debug.log("error setting up channel", ex);
1890     // no-op
1891   }
1892   this.c.running = true;
1893   this._chan.asyncOpen(this, null);
1894 }
1895
1896 Connection.prototype = {
1897   _interfaces: [
1898     Ci.nsISupports,
1899     Ci.nsISupportsWeakReference,
1900     Ci.nsIWeakReference,
1901     Ci.nsICancelable,
1902     Ci.nsIInterfaceRequestor,
1903     Ci.nsIStreamListener,
1904     Ci.nsIRequestObserver,
1905     Ci.nsIProgressEventSink,
1906     Ci.nsIChannelEventSink,
1907     Ci.nsIFTPEventSink
1908   ],
1909  
1910   cantCount: false,
1911
1912   QueryInterface: function DL_QI(iid) {
1913       if (this._interfaces.some(function(i) { return iid.equals(i); })) {
1914         return this;
1915       }
1916       throw Components.results.NS_ERROR_NO_INTERFACE;
1917   },
1918   // nsISupportsWeakReference
1919   GetWeakReference: function DL_GWR() {
1920     return this;
1921   },
1922   // nsIWeakReference
1923   QueryReferent: function DL_QR(uuid) {
1924     return this.QueryInterface(uuid);
1925   },
1926   // nsICancelable
1927   cancel: function DL_cancel(aReason) {
1928     try {
1929       if (this._closed) {
1930         return;
1931       }
1932       Debug.logString("cancel");
1933       if (!aReason) {
1934         aReason = NS_ERROR_BINDING_ABORTED;
1935       }
1936       this._chan.cancel(aReason);
1937       this._closed = true;
1938     }
1939     catch (ex) {
1940       Debug.log("cancel", ex);
1941     }
1942   },
1943   // nsIInterfaceRequestor
1944   getInterface: function DL_getInterface(iid) {
1945     if (iid.equals(Ci.nsIAuthPrompt)) {
1946       return Prompts.authPrompter;
1947     }
1948     if (iid.equals(Ci.nsIPrompt)) {
1949       return Prompts.prompter;
1950     }
1951     // for 1.9
1952     /* this one makes minefield ask for the password again and again :p
1953     if ('nsIAuthPromptProvider' in Ci && iid.equals(Ci.nsIAuthPromptProvider)) {
1954       return Prompts.prompter.QueryInterface(Ci.nsIAuthPromptProvider);
1955     }*/
1956     // for 1.9
1957     if ('nsIAuthPrompt2' in Ci && iid.equals(Ci.nsIAuthPrompt2)) {
1958       return Prompts.authPrompter.QueryInterface(Ci.nsIAuthPrompt2);
1959     }
1960     try {
1961       return this.QueryInterface(iid);
1962     }
1963     catch (ex) {
1964       Debug.log("interface not implemented: " + iid, ex);
1965       throw ex;
1966     }
1967   },
1968
1969   // nsIChannelEventSink
1970   onChannelRedirect: function DL_onChannelRedirect(oldChannel, newChannel, flags) {
1971     if (!this.isInfoGetter) {
1972       return;
1973     }
1974     try {
1975       this._chan == newChannel;
1976       this.url.url = newChannel.URI.spec;
1977       this.d.fileName = this.url.usable.getUsableFileName();
1978     }
1979     catch (ex) {
1980       // no-op
1981     }
1982   },
1983  
1984   // nsIStreamListener
1985   onDataAvailable: function DL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) {
1986     if (this._closed) {
1987       throw 0x804b0002; // NS_BINDING_ABORTED;
1988     }
1989     try {
1990       // we want to kill ftp chans as well which do not seem to respond to cancel correctly.
1991       if (!this.c.write(aInputStream, aCount)) {
1992         // we already got what we wanted
1993         this.cancel();
1994       }
1995     }
1996     catch (ex) {
1997       Debug.log('onDataAvailable', ex);
1998       this.d.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
1999     }
2000   },
2001  
2002   OnFTPControlLog: function(server, msg) {},
2003
2004   handleError: function DL_handleError() {
2005     let c = this.c;
2006     let d = this.d;
2007    
2008     c.cancel();
2009     d.dumpScoreboard();
2010     if (d.chunks.indexOf(c) == -1) {
2011       // already killed;
2012       return true;
2013     }
2014
2015     Debug.logString("handleError: problem found; trying to recover");
2016    
2017     if (d.urlManager.markBad(this.url)) {
2018       Debug.logString("handleError: fresh urls available, kill this one and use another!");
2019       d.timeLastProgress = Utils.getTimestamp();
2020       return true;
2021     }
2022    
2023     Debug.logString("affected: " + c);
2024    
2025     let max = -1, found = -1;
2026     for (let i = 0; i < d.chunks.length; ++i) {
2027       let cmp = d.chunks[i];
2028       if (cmp.start < c.start && cmp.start > max) {
2029         found = i;
2030         max = cmp.start;
2031       }
2032     }
2033     if (found > -1) {
2034       Debug.logString("handleError: found joinable chunk; recovering suceeded, chunk: " + found);
2035       d.chunks[found].end = c.end;
2036       if (--d.maxChunks == 1) {
2037         //d.resumable = false;
2038       }
2039       d.chunks = d.chunks.filter(function(ch) { return ch != c; });
2040       d.chunks.sort(function(a,b) { return a.start - b.start; });
2041      
2042       // check for overlapping ranges we might have created
2043       // otherwise we'll receive a size mismatch
2044       // this means that we're gonna redownload an already finished chunk...
2045       for (let i = d.chunks.length - 2; i > -1; --i) {
2046         let c1 = d.chunks[i], c2 = d.chunks[i + 1];
2047         if (c1.end >= c2.end) {
2048           if (c2.running) {
2049             // should never ever happen :p
2050             d.dumpScoreboard();
2051             Debug.logString("overlapping:\n" + c1 + "\n" + c2);
2052             d.fail("Internal error", "Please notify the developers that there were 'overlapping chunks'!", "Internal error (please report)");
2053             return false;
2054           }
2055           d.chunks.splice(i + 1, 1);
2056         }
2057       }
2058       let ac = 0;
2059       d.chunks.forEach(function(c) { if (c.running) { ++ac; }});
2060       d.activeChunks = ac;
2061       c.close();
2062      
2063       d.save();
2064       d.dumpScoreboard();
2065       return true;
2066     }
2067     return false;
2068   },
2069   handleHttp: function DL_handleHttp(aChannel) {
2070     let c = this.c;
2071     let d = this.d;
2072    
2073     let code = 0, status = 'Server returned nothing';
2074     try {
2075       code = aChannel.responseStatus;
2076       status = aChannel.responseStatusText;
2077     }
2078     catch (ex) {
2079       return true;
2080     }
2081      
2082     if (code >= 400) {
2083       if (!this.handleError()) {
2084         Debug.log("handleError: Cannot recover from problem!", code);
2085         if ([401, 402, 407, 500, 502, 503, 504].indexOf(code) != -1) {
2086           Debug.log("we got temp failure!", code);
2087           d.pause();
2088           d.markAutoRetry();
2089           d.status = code >= 500 ? _('temperror') : _('autherror');
2090         }
2091         else if (code == 450) {
2092           d.fail(
2093             _('pcerrortitle'),
2094             _('pcerrortext'),
2095             _('pcerrortitle')
2096           );
2097         }
2098         else {
2099           var file = d.fileName.length > 50 ? d.fileName.substring(0, 50) + "..." : d.fileName;
2100           code = Utils.formatNumber(code, 3);
2101           d.fail(
2102             _("error", [code]),
2103             _("failed", [file]) + " " + _("sra", [code]) + ": " + status,
2104             _("error", [code])
2105           );
2106         }
2107         // any data that we got over this channel should be considered "corrupt"
2108         c.rollback();
2109         d.save();
2110       }
2111       return false;
2112     }
2113
2114     // not partial content altough we are multi-chunk
2115     if (code != 206 && !this.isInfoGetter) {
2116       Debug.log(d + ": Server returned a " + aChannel.responseStatus + " response instead of 206", this.isInfoGetter);
2117      
2118       d.resumable = false;
2119
2120       if (!this.handleError()) {
2121         vis = {value: '', visitHeader: function(a,b) { this.value += a + ': ' + b + "\n"; }};
2122         aChannel.visitRequestHeaders(vis);
2123         Debug.logString("Request Headers\n\n" + vis.value);
2124         vis.value = '';
2125         aChannel.visitResponseHeaders(vis);
2126         Debug.logString("Response Headers\n\n" + vis.value);
2127         d.cancel();
2128         d.resumable = false;
2129         d.safeRetry();
2130         return false;
2131       }
2132     }
2133
2134     var visitor = null;
2135     try {
2136       visitor = d.visitors.visit(aChannel);
2137     }
2138     catch (ex) {
2139       Debug.log("header failed! " + d, ex);
2140       // restart download from the beginning
2141       d.cancel();
2142       d.resumable = false;
2143       d.safeRetry();
2144       return false;
2145     }
2146    
2147     if (!this.isInfoGetter) {
2148       return false;
2149     }
2150
2151     if (visitor.type) {
2152       d.contentType = visitor.type;
2153     }
2154
2155     // compression?
2156     if (['gzip', 'deflate'].indexOf(visitor.encoding) != -1 && !d.contentType.match(/gzip/i) && !d.fileName.match(/\.gz$/i)) {
2157       d.compression = visitor.encoding;
2158     }
2159     else {
2160       d.compression = null;
2161     }
2162
2163     // accept range
2164     d.resumable &= visitor.acceptRanges;
2165
2166     if (visitor.type && visitor.type.search(/application\/metalink\+xml/) != -1) {
2167       d.isMetalink = true;
2168       d.resumable = false;
2169     }
2170
2171     if (visitor.contentlength > 0) {
2172       d.totalSize = visitor.contentlength;
2173     } else {
2174       d.totalSize = 0;
2175     }
2176    
2177     if (visitor.fileName && visitor.fileName.length > 0) {
2178       // if content disposition hasn't an extension we use extension of URL
2179       let newName = visitor.fileName;
2180       let ext = this.url.usable.getExtension();
2181       if (visitor.fileName.lastIndexOf('.') == -1 && ext) {
2182         newName += '.' + ext;
2183       }
2184       let charset = visitor.overrideCharset ? visitor.overrideCharset : this.url.charset;
2185       d.fileName = DTA_URLhelpers.decodeCharset(newName, charset).getUsableFileName();
2186     }
2187
2188     return false;
2189   },
2190  
2191   // Generic handler for now :p
2192   handleFtp: function  DL_handleFtp(aChannel) {
2193     return this.handleGeneric(aChannel);
2194   },
2195  
2196   handleGeneric: function DL_handleGeneric(aChannel) {
2197     var c = this.c;
2198     var d = this.d;
2199    
2200     // hack: determine if we are a multi-part chunk,
2201     // if so something bad happened, 'cause we aren't supposed to be multi-part
2202     if (c.start != 0 && d.is(RUNNING)) {
2203       if (!this.handleError()) {
2204         Debug.log(d + ": Server error or disconnection", "(type 1)");
2205         d.status = _("servererror");
2206         d.markAutoRetry();
2207         d.pause();
2208       }
2209       return false;
2210     }     
2211      
2212     // try to get the size anyway ;)
2213     try {
2214       let pb = aChannel.QueryInterface(Ci.nsIPropertyBag2);
2215       d.totalSize = Math.max(pb.getPropertyAsInt64('content-length'), 0);
2216     }
2217     catch (ex) {
2218       try {
2219         d.totalSize = Math.max(aChannel.contentLength, 0);
2220       }
2221       catch (ex) {
2222         d.totalSize = 0;
2223       }
2224     }
2225     d.resumable = false;
2226     return false;
2227   },
2228  
2229   //nsIRequestObserver,
2230   _supportedChannels: [
2231     {i:Ci.nsIHttpChannel, f:'handleHttp'},
2232     {i:Ci.nsIFTPChannel, f:'handleFtp'},
2233     {i:Ci.nsIChannel, f:'handleGeneric'}
2234   ],
2235   onStartRequest: function DL_onStartRequest(aRequest, aContext) {
2236     let c = this.c;
2237     let d = this.d;
2238     Debug.logString('StartRequest: ' + c);
2239  
2240     this.started = true;
2241     try {
2242       for (let i = 0, e = this._supportedChannels.length; i < e; ++i) {
2243         let sc = this._supportedChannels[i];
2244         let chan = null;
2245         try {
2246           chan = aRequest.QueryInterface(sc.i);
2247         }
2248         catch (ex) {
2249           continue;
2250         }
2251         if (chan) {
2252           if ((this.rexamine = this[sc.f](chan))) {
2253              return;
2254           }
2255           break;
2256         }         
2257       }
2258
2259       if (this.isInfoGetter) {
2260         // Checks for available disk space.
2261         
2262         if (d.fileName.getExtension() == 'metalink') {
2263           d.isMetalink = true;
2264           d.resumable = true;
2265         }       
2266        
2267         var tsd = d.totalSize;
2268         try {
2269           if (tsd) {
2270             let tmp = Prefs.tempLocation, vtmp = 0;
2271             if (tmp) {
2272               vtmp = Utils.validateDir(tmp);
2273               if (!vtmp && Utils.getFreeDisk(vtmp) < tsd) {
2274                 d.fail(_("ndsa"), _("spacetemp"), _("freespace"));
2275                 return;
2276               }
2277             }
2278             let realDest = Utils.validateDir(d.destinationPath);
2279             if (!realDest) {
2280               throw new Error("invalid destination folder");
2281             }
2282             var nsd = Utils.getFreeDisk(realDest);
2283             // Same save path or same disk (we assume that tmp.avail == dst.avail means same disk)
2284             // simply moving should succeed
2285             if (d.compression && (!tmp || Utils.getFreeDisk(vtmp) == nsd)) {
2286               // we cannot know how much space we will consume after decompressing.
2287               // so we assume factor 1.0 for the compressed and factor 1.5 for the decompressed file.
2288               tsd *= 2.5;
2289             }
2290             if (nsd < tsd) {
2291               Debug.logString("nsd: " +  nsd + ", tsd: " + tsd);
2292               d.fail(_("ndsa"), _("spacedir"), _("freespace"));
2293               return;
2294             }
2295           }
2296         }
2297         catch (ex) {
2298           Debug.log("size check threw", ex);
2299           d.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
2300           return;
2301         }
2302        
2303         if (!d.totalSize) {
2304           d.resumable = false;         
2305           this.cantCount = true;
2306         }
2307         if (!d.resumable) {
2308           d.maxChunks = 1;
2309         }
2310         c.end = d.totalSize - 1;
2311         delete this.isInfoGetter;
2312        
2313         // Explicitly trigger rebuildDestination here, as we might have received
2314         // a html content type and need to rewrite the file
2315         d.rebuildDestination();
2316         ConflictManager.resolve(d);
2317       }
2318      
2319       if (d.resumable && !d.is(CANCELED)) {
2320         d.resumeDownload();
2321       }
2322     }
2323     catch (ex) {
2324       Debug.log("onStartRequest", ex);
2325     }
2326   },
2327   onStopRequest: function DL_onStopRequest(aRequest, aContext, aStatusCode) {
2328     try {
2329       Debug.logString('StopRequest');
2330     }
2331     catch (ex) {
2332       return;
2333     }
2334    
2335     // shortcuts
2336     let c = this.c;
2337     let d = this.d;
2338     c.close();
2339    
2340     if (d.chunks.indexOf(c) == -1) {
2341       return;
2342     }
2343
2344     // update flags and counters
2345     d.refreshPartialSize();
2346     --d.activeChunks;
2347
2348     // check if we're complete now
2349     if (d.is(RUNNING) && d.chunks.every(function(e) { return e.complete; })) {
2350       if (!d.resumeDownload()) {
2351         d.state = FINISHING;
2352         Debug.logString(d + ": Download is complete!");
2353         d.finishDownload();
2354         return;
2355       }
2356     }
2357
2358     if (c.starter && -1 != [
2359       NS_ERROR_CONNECTION_REFUSED,
2360       NS_ERROR_UNKNOWN_HOST,
2361       NS_ERROR_NET_TIMEOUT,
2362       NS_ERROR_NET_RESET
2363     ].indexOf(aStatusCode)) {
2364       Debug.log(d + ": Server error or disconnection", "(type 3)");
2365       d.pause();
2366       d.status = _("servererror");
2367       d.markAutoRetry();       
2368       return;
2369     }   
2370
2371     // routine for normal chunk
2372     Debug.logString(d + ": Chunk " + c.start + "-" + c.end + " finished.");
2373    
2374     // rude way to determine disconnection: if connection is closed before download is started we assume a server error/disconnection
2375     if (c.starter && d.is(RUNNING)) {
2376       if (!d.urlManager.markBad(this.url)) {
2377         Debug.log(d + ": Server error or disconnection", "(type 2)");
2378         d.pause();
2379         d.status = _("servererror");
2380         d.markAutoRetry();       
2381       }
2382       else {
2383         Debug.log("caught bad server", d.toString());
2384         d.cancel();
2385         d.safeRetry();
2386       }
2387       return;     
2388     }
2389
2390     if (!d.is(PAUSED, CANCELED, FINISHING) && d.chunks.length == 1 && d.chunks[0] == c) {
2391       if (d.resumable) {
2392         d.pause();
2393         d.markAutoRetry();
2394         d.status = _('errmismatchtitle');
2395       }
2396       else {
2397         d.fail(
2398           _('errmismatchtitle'),
2399           _('errmismatchtext', [d.partialSize, d.totalSize]),
2400           _('errmismatchtitle')
2401         );
2402       }
2403       return;     
2404     }
2405     if (!d.is(PAUSED, CANCELED)) {
2406       d.resumeDownload();
2407     }
2408   },
2409
2410   // nsIProgressEventSink
2411   onProgress: function DL_onProgress(aRequest, aContext, aProgress, aProgressMax) {
2412     try {
2413       // shortcuts
2414       let c = this.c;
2415       let d = this.d;
2416      
2417       if (this.reexamine) {
2418         Debug.logString(d + ": reexamine");
2419         this.onStartRequest(aRequest, aContext);
2420         if (this.reexamine) {
2421           return;
2422         }
2423       }
2424
2425       // update download tree row
2426       if (d.is(RUNNING)) {
2427         d.refreshPartialSize();
2428
2429         if (!this.resumable && d.totalSize) {
2430           // basic integrity check
2431           if (d.partialSize > d.totalSize) {
2432             d.dumpScoreboard();
2433             Debug.logString(d + ": partialSize > totalSize" + "(" + d.partialSize + "/" + d.totalSize + "/" + ( d.partialSize - d.totalSize) + ")");
2434             d.fail(
2435               _('errmismatchtitle'),
2436               _('errmismatchtext', [d.partialSize, d.totalSize]),
2437               _('errmismatchtitle')
2438             );
2439             return;
2440           }
2441         }
2442         else {
2443           d.status = _("downloading");
2444         }
2445       }
2446     }
2447     catch(ex) {
2448       Debug.log("onProgressChange():", e);
2449     }
2450   },
2451   onStatus: function  DL_onStatus(aRequest, aContext, aStatus, aStatusArg) {}
2452 };
2453
2454 function startDownloads(start, downloads) {
2455
2456   var numbefore = Tree.rowCount - 1;
2457   const DESCS = ['description', 'ultDescription'];
2458  
2459   let g = downloads;
2460   if ('length' in downloads) {
2461     g = function() {
2462        for (let i = 0, e = downloads.length; i < e; ++i) {
2463         yield downloads[i];
2464        }
2465     }();
2466   }
2467
2468   let added = 0;
2469   let removeableTabs = {};
2470   Tree.beginUpdate();
2471   SessionManager.beginUpdate();
2472   for (let e in g) {
2473
2474     var desc = "";
2475     DESCS.some(
2476       function(i) {
2477         if (typeof(e[i]) == 'string' && e[i].length) {
2478           desc = e.description;
2479           return true;
2480         }
2481         return false;
2482       }
2483     );
2484    
2485     let qi = new QueueItem();
2486     let lnk = e.url;
2487     if (typeof lnk == 'string') {
2488       qi.urlManager = new UrlManager([new DTA_URL(lnk)]);
2489     }
2490     else if (lnk instanceof UrlManager) {
2491       qi.urlManager = lnk;
2492     }
2493     else {
2494       qi.urlManager = new UrlManager([lnk]);
2495     }
2496     qi.numIstance = e.numIstance;
2497  
2498     if (e.referrer) {
2499       try {
2500         qi.referrer = e.referrer.toURL();
2501       }
2502       catch (ex) {
2503         // We might have been fed with about:blank or other crap. so ignore.
2504       }
2505     }
2506     // only access the setter of the last so that we don't generate stuff trice.
2507     qi._pathName = e.dirSave.addFinalSlash();
2508     qi._description = desc ? desc : '';
2509     qi._mask = e.mask;
2510     if (e.fileName) {
2511       qi.fileName = e.fileName;
2512     }
2513     else {
2514       qi.fileName = qi.urlManager.usable.getUsableFileName();
2515     }
2516     if (e.startDate) {
2517       qi.startDate = e.startDate;
2518     }
2519     if (e.url.hash) {
2520       qi.hash = e.url.hash;
2521     }
2522     else if (e.hash) {
2523       qi.hash = e.hash;
2524     }
2525     else {
2526       qi.hash = null; // to initialize prettyHash
2527     }
2528
2529     let postData = ContentHandling.getPostDataFor(qi.urlManager.url.toURI());
2530     if (e.url.postData) {
2531       postData = e.url.postData;
2532     }
2533     if (postData) {
2534       qi.postData = postData;
2535     }   
2536
2537     qi.state = start ? QUEUED : PAUSED;
2538     if (qi.is(QUEUED)) {
2539       qi.status = _('inqueue');
2540     }
2541     else {
2542       qi.status = _('paused');
2543     }
2544     qi.save();   
2545     Tree.add(qi);
2546     ++added;
2547   }
2548   SessionManager.endUpdate();
2549   Tree.endUpdate();
2550
2551   var boxobject = Tree._box;
2552   boxobject.QueryInterface(Ci.nsITreeBoxObject);
2553   if (added <= boxobject.getPageLength()) {
2554     boxobject.scrollToRow(Tree.rowCount - boxobject.getPageLength());
2555   }
2556   else {
2557     boxobject.scrollToRow(numbefore);
2558   }
2559 }
2560 const FileOutputStream = Components.Constructor(
2561   '@mozilla.org/network/file-output-stream;1',
2562   'nsIFileOutputStream',
2563   'init'
2564 );
2565
2566 var ConflictManager = {
2567   _items: [],
2568   resolve: function CM_resolve(download, reentry) {
2569     if (!this._check(download)) {
2570       if (reentry) {
2571         download[reentry]();
2572       }
2573       return;
2574     }
2575     for (let i = 0; i < this._items.length; ++i) {
2576       if (this._items[i].download == download) {
2577         Debug.logString("conflict resolution updated to: " + reentry);
2578        
2579         this._items[i].reentry = reentry;
2580         return;
2581       }
2582     }
2583     Debug.logString("conflict resolution queued to: " + reentry);
2584     this._items.push({download: download, reentry: reentry});
2585     this._process();
2586   },
2587   _check: function CM__check(download) {
2588     let dest = new FileFactory(download.destinationFile);
2589     let sn = false;
2590     if (download.is(RUNNING)) {
2591       sn = Dialog.checkSameName(download, download.destinationFile);
2592     }
2593     Debug.logString("conflict check: " + sn + "/" + dest.exists() + " for " + download.destinationFile);
2594     return dest.exists() || sn;
2595   },
2596   _process: function CM__process() {
2597     if (this._processing) {
2598       return;
2599     }
2600     let cur;
2601     while (this._items.length) {
2602       cur = this._items[0];
2603       if (!this._check(cur.download)) {
2604         if (reentry) {
2605           cur.download[reentry]();
2606         }
2607         this._items.shift();
2608         continue;
2609       }
2610       break;
2611     }
2612     if (!this._items.length) {
2613       return;
2614     }
2615  
2616     if (Prefs.conflictResolution != 3) {
2617       this._return(Prefs.conflictResolution);
2618       return;
2619     }
2620     if (this._sessionSetting) {
2621       this._return(this._sessionSetting);
2622       return;
2623     }
2624     if (cur.download.shouldOverwrite) {
2625       this._return(1);
2626       return;
2627     }
2628    
2629     this._computeConflicts(cur);
2630
2631     var options = {
2632       url: cur.download.urlManager.usable.cropCenter(45),
2633       fn: cur.download.destinationName.cropCenter(45),
2634       newDest: cur.newDest.cropCenter(45)
2635     };
2636    
2637     this._processing = true;
2638    
2639     window.openDialog(
2640       "chrome://dta/content/dta/manager/conflicts.xul",
2641       "_blank",
2642       "chrome,centerscreen,resizable=no,dialog,close=no,dependent",
2643       options, this
2644     );
2645   },
2646   _computeConflicts: function CM__computeConflicts(cur) {
2647     let download = cur.download;
2648     download.conflicts = 0;
2649     let basename = download.destinationName;
2650     let newDest = new FileFactory(download.destinationFile);
2651     let i = 1;
2652     for (;; ++i) {
2653       newDest.leafName = Utils.formatConflictName(basename, i);
2654       if (!newDest.exists() && (!download.is(RUNNING) || !Dialog.checkSameName(this, newDest.path))) {
2655         break;
2656       }
2657     }
2658     cur.newDest = newDest.leafName;
2659     cur.conflicts = i; 
2660   },
2661   _returnFromDialog: function CM__returnFromDialog(option, type) {
2662     if (type == 1) {
2663       this._sessionSetting = option;
2664     }
2665     if (type == 2) {
2666       Preferences.setDTA('conflictresolution', option);
2667     }   
2668     this._return(option);
2669   },
2670   _return: function CM__return(option) {
2671     let cur = this._items[0];
2672     switch (option) {
2673       /* rename */    case 0: this._computeConflicts(cur); cur.download.conflicts = cur.conflicts; break;
2674       /* overwrite */ case 1: cur.download.shouldOverwrite = true; break;
2675       /* skip */      default: cur.download.cancel(_('skipped')); break;
2676     }
2677     if (cur.reentry) {
2678       cur.download[cur.reentry]();
2679     }
2680     this._items.shift();
2681     this._processing = false;
2682     this._process();
2683   }
2684 };
Note: See TracBrowser for help on using the browser.