Changeset 1037

Show
Ignore:
Timestamp:
2008-08-08 04:58:20 (4 months ago)
Author:
MaierMan
Message:

#259: Implement FTP resuming

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/chrome/content/dta/manager.js

    r1032 r1037  
    4747const NS_ERROR_NET_TIMEOUT = NS_ERROR_MODULE_NETWORK + 14; 
    4848const NS_ERROR_NET_RESET = NS_ERROR_MODULE_NETWORK + 20; 
    49  
     49const NS_ERROR_FTP_CWD = NS_ERROR_MODULE_NETWORK + 22; 
    5050const Cc = Components.classes; 
    5151const Ci = Components.interfaces; 
     
    7575 
    7676// in use by chunk.writer... 
    77 // in use by decompressor... beware, actual size might be more than twice as big! 
     77// in use by decompressor... beware, actual size might be more than twice as 
     78// big! 
    7879const MAX_BUFFER_SIZE = 5 * 1024 * 1024; 
    7980const MIN_BUFFER_SIZE = 1 * 1024 * 1024; 
     
    222223                                speed /= d.speeds.length; 
    223224                                 
    224                                 // Calculate estimated time                                     
     225                                // Calculate estimated time 
    225226                                if (advanced != 0 && d.totalSize > 0) { 
    226227                                        let remaining = Math.ceil((d.totalSize - d.partialSize) / speed); 
     
    316317 
    317318                        if (!this.offline) { 
    318                                 // XXX Improve                                  
     319                                // XXX Improve 
    319320                                if (Prefs.autoRetryInterval) { 
    320321                                        for (let d in Tree.all) { 
     
    514515                if (!Prefs.tempLocation || Preferences.getExt("tempLocation", '') != '') { 
    515516                        // cannot perform this action if we don't use a temp file 
    516                         // there might be far too many directories containing far too many tmpFiles. 
     517                        // there might be far too many directories containing far too many 
     518                        // tmpFiles. 
    517519                        // or part files from other users. 
    518520                        return; 
     
    645647        } 
    646648}; 
     649 
    647650function Visitor() { 
    648651        // sanity check 
     
    651654        } 
    652655 
    653         var nodes = arguments[0]; 
     656        let nodes = arguments[0]; 
    654657        for (x in nodes) { 
    655658                if (!name || !(name in this.cmpKeys))   { 
     
    661664 
    662665Visitor.prototype = { 
     666        compare: function vi_compare(v) { 
     667                if (!(v instanceof Visitor)) { 
     668                        return; 
     669                } 
     670 
     671                for (x in this.cmpKeys) { 
     672                        // we don't have this header 
     673                        if (!(x in this)) { 
     674                                continue; 
     675                        } 
     676                        // v does not have this header 
     677                        else if (!(x in v)) { 
     678                                // allowed to be missing? 
     679                                if (this.cmpKeys[x]) { 
     680                                        continue; 
     681                                } 
     682                                Debug.logString(x + " missing"); 
     683                                throw new Exception(x + " is missing"); 
     684                        } 
     685                        // header is there, but differs 
     686                        else if (this[x] != v[x]) { 
     687                                Debug.logString(x + " nm: [" + this[x] + "] [" + v[x] + "]"); 
     688                                throw new Exception("Header " + x + " doesn't match"); 
     689                        } 
     690                } 
     691        }, 
     692        save: function vi_save(node) { 
     693                var rv = {}; 
     694                // salva su file le informazioni sugli headers 
     695                for (x in this.cmpKeys) { 
     696                        if (!(x in this)) { 
     697                                continue; 
     698                        } 
     699                        rv[x] = this[x]; 
     700                } 
     701                return rv; 
     702        } 
     703}; 
     704 
     705function HttpVisitor() { 
     706        Visitor.apply(this, arguments); 
     707} 
     708 
     709HttpVisitor.prototype = { 
    663710        cmpKeys: { 
    664                 'etag': true, // must not be modified from 200 to 206: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7 
    665                 //'content-length': false, 
     711                'etag': true, // must not be modified from 200 to 206: 
     712                                                                        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7 
     713                // 'content-length': false, 
    666714                'content-type': true, 
    667715                'last-modified': true, // may get omitted later, but should not change 
    668                 'content-encoding': true // must not change, or download will become corrupt. 
    669         }, 
    670         type: null, 
    671         overrideCharset: null, 
    672         encoding: null, 
    673         fileName: null, 
    674         acceptRanges: 'bytes', 
    675         contentLength: 0, 
    676         time: null, 
    677  
     716                'content-encoding': true // must not change, or download will become 
     717                                                                                                                        // corrupt. 
     718        }, 
    678719        QueryInterface: function(aIID) { 
    679720                if ( 
     
    732773                        } 
    733774                        if (header == 'etag') { 
    734                                 // strip off the "inode"-part apache and others produce, as mirrors/caches usually provide different/wrong numbers here :p 
     775                                // strip off the "inode"-part apache and others produce, as 
     776                                // mirrors/caches usually provide different/wrong numbers here :p 
    735777                                this[header] = aValue 
    736778                                        .replace(/^(?:[Ww]\/)?"(.+)"$/, '$1') 
     
    766808                catch (ex) { 
    767809                } 
    768         }, 
    769         compare: function vi_compare(v) { 
    770                 if (!(v instanceof Visitor)) { 
    771                         return; 
    772                 } 
    773  
    774                 for (x in this.cmpKeys) { 
    775                         // we don't have this header 
    776                         if (!(x in this)) { 
    777                                 continue; 
    778                         } 
    779                         // v does not have this header 
    780                         else if (!(x in v)) { 
    781                                 // allowed to be missing? 
    782                                 if (this.cmpKeys[x]) { 
    783                                         continue; 
    784                                 } 
    785                                 Debug.logString(x + " missing"); 
    786                                 throw new Exception(x + " is missing"); 
    787                         } 
    788                         // header is there, but differs 
    789                         else if (this[x] != v[x]) { 
    790                                 Debug.logString(x + " nm: [" + this[x] + "] [" + v[x] + "]"); 
    791                                 throw new Exception("Header " + x + " doesn't match"); 
    792                         } 
    793                 } 
    794         }, 
    795         save: function vi_save(node) { 
    796                 var rv = {}; 
    797                 // salva su file le informazioni sugli headers 
    798                 for (x in this.cmpKeys) { 
    799                         if (!(x in this)) { 
    800                                 continue; 
    801                         } 
    802                         rv[x] = this[x]; 
    803                 } 
    804                 return rv; 
    805810        } 
    806811}; 
    807812 
     813Utils.merge(HttpVisitor.prototype, Visitor.prototype); 
     814 
     815function FtpVisitor() { 
     816        Visitor.apply(this, arguments); 
     817} 
     818 
     819FtpVisitor.prototype = { 
     820        cmpKeys: { 
     821                'etag': true, 
     822        }, 
     823        time: null, 
     824        visitChan: function fv_visitChan(chan) { 
     825                try { 
     826                        this.etag = chan.QueryInterface(Ci.nsIResumableChannel).entityID; 
     827                        let m = this.etag.match(/\/(\d{4})(\d{2})(\d{2})(?:(\d{2})(\d{2})(?:(\d{2})))?$/); 
     828                        if (m) { 
     829                                let time = m[1] + '/' + m[2] + '/' + m[3]; 
     830                                if (m.length >= 4) { 
     831                                        time += ' ' + m[4] + ':' + m[5]; 
     832                                        if (m.length >= 7) { 
     833                                                time += ':' + m[6]; 
     834                                        } 
     835                                        this.time = Utils.getTimestamp(time); 
     836                                        Debug.logString(this.time); 
     837                                } 
     838                        } 
     839                } 
     840                catch (ex) { 
     841                        Debug.log("visitChan:", ex); 
     842                } 
     843        } 
     844}; 
     845 
     846Utils.merge(FtpVisitor.prototype, Visitor.prototype); 
     847 
    808848/** 
    809849 * Visitor Manager c'tor 
     850 *  
    810851 * @author Nils 
    811852 */ 
     
    818859VisitorManager.prototype = { 
    819860        /** 
    820          * Loads a ::save'd JS Array 
    821          * Will silently bypass failed items! 
     861         * Loads a ::save'd JS Array Will silently bypass failed items! 
     862         *  
    822863         * @author Nils 
    823864         */ 
     
    825866                for each (let n in nodes) { 
    826867                        try { 
    827                                 this._visitors[n.url] = new Visitor(n.values); 
     868                                let uri = IOService.newURI(n.url, null, null); 
     869                                switch (uri.scheme) { 
     870                                case 'http': 
     871                                        this._visitors[n.url] = new HttpVisitor(n.values); 
     872                                        break; 
     873                                case 'ftp': 
     874                                        this._visitors[n.url] = new FtpVisitor(n.values); 
     875                                        break; 
     876                                } 
    828877                        } 
    829878                        catch (ex) { 
     
    834883        /** 
    835884         * Saves/serializes the Manager and associated Visitors to an JS Array 
     885         *  
    836886         * @return A ::load compatible Array 
    837887         * @author Nils 
     
    854904        /** 
    855905         * Visit and compare a channel 
     906         *  
    856907         * @returns visitor for channel 
    857          * @throws Exception if comparision yield a difference (i.e. channels are not "compatible") 
     908         * @throws Exception 
     909         *           if comparision yield a difference (i.e. channels are not 
     910         *           "compatible") 
    858911         * @author Nils 
    859912         */ 
    860913        visit: function vm_visit(chan) { 
    861                 var url = chan.URI.spec; 
    862  
    863                 var visitor = new Visitor(); 
    864                 chan.visitResponseHeaders(visitor); 
    865                 if (url in this._visitors) 
    866                 { 
    867                                 this._visitors[url].compare(visitor); 
     914                let url = chan.URI.spec; 
     915                 
     916                let visitor; 
     917                switch(chan.URI.scheme) { 
     918                case 'http': 
     919                        visitor = new HttpVisitor(); 
     920                        chan.visitResponseHeaders(visitor); 
     921                        break; 
     922                case 'ftp': 
     923                        visitor = new FtpVisitor(chan); 
     924                        visitor.visitChan(chan); 
     925                        break; 
     926                } 
     927                 
     928                if (url in this._visitors) { 
     929                        this._visitors[url].compare(visitor); 
    868930                } 
    869931                return (this._visitors[url] = visitor); 
     
    871933        /** 
    872934         * return the first timestamp registered with a visitor 
    873          * @throws Exception if no timestamp found 
     935         *  
     936         * @throws Exception 
     937         *           if no timestamp found 
    874938         * @author Nils 
    875939         */ 
     
    10391103 
    10401104        /** 
    1041          *Takes one or more state indicators and returns if this download is in state of any of them 
     1105         * Takes one or more state indicators and returns if this download is in state 
     1106         * of any of them 
    10421107         */ 
    10431108        is: function QI_is(state) { 
     
    15951660                Debug.logString("resumeDownload: " + this); 
    15961661                function cleanChunks(d) { 
    1597                         // merge finished chunks together, so that the scoreboard does not bloat that much 
     1662                        // merge finished chunks together, so that the scoreboard does not bloat 
     1663                        // that much 
    15981664                        for (let i = d.chunks.length - 2; i > -1; --i) { 
    15991665                                let c1 = d.chunks[i], c2 = d.chunks[i + 1]; 
     
    16301696                        var rv = false; 
    16311697 
    1632                         // we didn't load up anything so let's start the main chunk (which will grab the info) 
     1698                        // we didn't load up anything so let's start the main chunk (which will 
     1699                        // grab the info) 
    16331700                        if (this.chunks.length == 0) { 
    16341701                                downloadNewChunk(this, 0, 0, true); 
     
    19522019        this.url = d.urlManager.getURL(); 
    19532020        var referrer = d.referrer; 
    1954         Debug.logString("starting: " + this.url.url); 
     2021        Debug.logString("starting: " + this.url.url.spec); 
    19552022 
    19562023        this._chan = IOService.newChannelFromURI(this.url.url); 
     
    19652032                // no-op 
    19662033        } 
    1967         try { 
    1968                 let http = this._chan.QueryInterface(Ci.nsIHttpChannel); 
    1969                 if (c.start + c.written > 0) { 
    1970                         http.setRequestHeader('Range', 'bytes=' + (c.start + c.written) + "-", false); 
    1971                 } 
    1972                 if (this.isInfoGetter && !d.fromMetalink) { 
    1973                         http.setRequestHeader('Accept', 'application/metalink+xml;q=0.9', true); 
    1974                 } 
    1975                 if (referrer instanceof Ci.nsIURI) { 
    1976                         http.referrer = referrer; 
    1977                 } 
    1978                 http.setRequestHeader('Keep-Alive', '', false); 
    1979                 http.setRequestHeader('Connection', 'close', false); 
    1980                 if (d.postData) { 
    1981                         let uc = http.QueryInterface(Ci.nsIUploadChannel); 
    1982                         uc.setUploadStream(new StringInputStream(d.postData, d.postData.length), null, -1); 
    1983                         http.requestMethod = 'POST'; 
    1984                 }                         
     2034        if (this._chan instanceof Ci.nsIHttpChannel) { 
     2035                try { 
     2036                        Debug.logString("http"); 
     2037                        let http = this._chan.QueryInterface(Ci.nsIHttpChannel); 
     2038                        if (c.start + c.written > 0) { 
     2039                                http.setRequestHeader('Range', 'bytes=' + (c.start + c.written) + "-", false); 
     2040                        } 
     2041                        if (this.isInfoGetter && !d.fromMetalink) { 
     2042                                http.setRequestHeader('Accept', 'application/metalink+xml;q=0.9', true); 
     2043                        } 
     2044                        if (referrer instanceof Ci.nsIURI) { 
     2045                                http.referrer = referrer; 
     2046                        } 
     2047                        http.setRequestHeader('Keep-Alive', '', false); 
     2048                        http.setRequestHeader('Connection', 'close', false); 
     2049                        if (d.postData) { 
     2050                                let uc = http.QueryInterface(Ci.nsIUploadChannel); 
     2051                                uc.setUploadStream(new StringInputStream(d.postData, d.postData.length), null, -1); 
     2052                                http.requestMethod = 'POST'; 
     2053                        }                         
     2054                } 
     2055                catch (ex) { 
     2056                        Debug.log("error setting up http channel", ex); 
     2057                        // no-op 
     2058                } 
    19852059        } 
    1986         catch (ex) { 
    1987                 Debug.log("error setting up channel", ex); 
    1988                 // no-op 
     2060        else if (this._chan instanceof Ci.nsIFTPChannel) { 
     2061                try { 
     2062                        let ftp = this._chan.QueryInterface(Ci.nsIFTPChannel); 
     2063                        if (c.start + c.written > 0) { 
     2064                                        let resumable = ftp.QueryInterface(Ci.nsIResumableChannel); 
     2065                                        resumable.resumeAt(c.start + c.written, ''); 
     2066                        }                                
     2067                } 
     2068                catch (ex) { 
     2069                        Debug.log('error setting up ftp channel', ex); 
     2070                } 
    19892071        } 
    19902072        this.c.running = true; 
     
    20122094                        return this; 
    20132095                } 
     2096                Debug.log("Interface not implemented " + iid, Components.results.NS_ERROR_NO_INTERFACE); 
    20142097                throw Components.results.NS_ERROR_NO_INTERFACE; 
    20152098        }, 
     
    20572140                        return AuthPrompts.prompter; 
    20582141                } 
    2059                 // for 1.9 
    2060                 /* this one makes minefield ask for the password again and again :p 
    2061                 if ('nsIAuthPromptProvider' in Ci && iid.equals(Ci.nsIAuthPromptProvider)) { 
    2062                         return AuthPrompts.prompter.QueryInterface(Ci.nsIAuthPromptProvider); 
    2063                 }*/ 
    2064                 // for 1.9 
    20652142                if ('nsIAuthPrompt2' in Ci && iid.equals(Ci.nsIAuthPrompt2)) { 
    20662143                        return AuthPrompts.authPrompter.QueryInterface(Ci.nsIAuthPrompt2); 
     
    20962173                } 
    20972174                try { 
    2098                         // we want to kill ftp chans as well which do not seem to respond to cancel correctly. 
     2175                        // we want to kill ftp chans as well which do not seem to respond to 
     2176                        // cancel correctly. 
    20992177                        if (!this.c.write(aInputStream, aCount)) { 
    21002178                                // we already got what we wanted 
     
    21092187         
    21102188        // nsIFTPEventSink 
    2111         OnFTPControlLog: function(server, msg) {}, 
     2189        OnFTPControlLog: function(server, msg) { 
     2190                /* 
     2191                 * Very hacky :p If we don't handle it here, then nsIFTPChannel will + try 
     2192                 * to CWD to the file (d'oh) + afterwards ALERT (modally) that the CWD 
     2193                 * didn't succeed (double-d'oh) 
     2194                 */ 
     2195                if (!server) { 
     2196                        this._wasRetr = /^RETR/.test(msg) || /^REST/.test(msg); 
     2197                } 
     2198                if (server && this._wasRetr && /^[45]/.test(msg)) { 
     2199                        Debug.logString("Server didn't allow retrieving stuff!"); 
     2200                        if (!this.handleError()) { 
     2201                                d.fail(_('servererror'), _('ftperrortext'), _('servererror')); 
     2202                        } 
     2203                } 
     2204                // Debug.logString((server ? 's' : 'c') + ': ' + msg); 
     2205        }, 
    21122206         
    21132207        handleError: function DL_handleError() { 
     
    21312225                 
    21322226                Debug.logString("affected: " + c); 
     2227                d.dumpScoreboard(); 
    21332228                 
    2134                 let max = -1, found = -1
     2229                let max = -1, found = null
    21352230                for each (let cmp in d.chunks) { 
    21362231                        if (cmp.start < c.start && cmp.start > max) { 
    2137                                 found = i
     2232                                found = cmp
    21382233                                max = cmp.start; 
    21392234                        } 
    21402235                } 
    2141                 if (found > -1) { 
     2236                if (found) { 
    21422237                        Debug.logString("handleError: found joinable chunk; recovering suceeded, chunk: " + found); 
    2143                         d.chunks[found].end = c.end; 
     2238                        found.end = c.end; 
    21442239                        if (--d.maxChunks == 1) { 
    2145                                 //d.resumable = false; 
    2146                         } 
    2147                         d.chunks = d.chunks.filter(function(ch) { return ch != c; }); 
    2148                         d.chunks.sort(function(a,b) { return a.start - b.start; }); 
     2240                                // d.resumable = false; 
     2241                        } 
     2242                        d.chunks = d.chunks.filter(function(ch) ch != c); 
     2243                        d.chunks.sort(function(a, b) a.start - b.start); 
    21492244                         
    21502245                        // check for overlapping ranges we might have created 
     
    21732268                        return true; 
    21742269                } 
     2270                Debug.logString("recovery failed"); 
    21752271                return false; 
    21762272        }, 
     
    22992395        // Generic handler for now :p 
    23002396        handleFtp: function  DL_handleFtp(aChannel) { 
    2301                 return this.handleGeneric(aChannel); 
     2397                let c = this.c; 
     2398                let d = this.d; 
     2399                try { 
     2400                        let pb = aChannel.QueryInterface(Ci.nsIPropertyBag2); 
     2401                        d.totalSize = Math.max(pb.getPropertyAsInt64('content-length'), 0); 
     2402                } 
     2403                catch (ex) { 
     2404                        d.totalSize = 0; 
     2405                        d.resumable = false; 
     2406                } 
     2407                 
     2408                try { 
     2409                        aChannel.QueryInterface(Ci.nsIResumableChannel).entityID; 
     2410                } 
     2411                catch (ex) { 
     2412                        Debug.logString("likely not resumable or connection refused!"); 
     2413                        if (!this.handleError()) { 
     2414                                // restart download from the beginning 
     2415                                d.fail(_('servererror'), _('ftperrortext'), _('servererror'));  
     2416                                return false; 
     2417                        } 
     2418                } 
     2419                 
     2420                try { 
     2421                        let visitor = d.visitors.visit(aChannel.QueryInterface(Ci.nsIChannel)); 
     2422                } 
     2423                catch (ex) { 
     2424                        Debug.log("header failed! " + d, ex); 
     2425                        // restart download from the beginning 
     2426                        d.cancel(); 
     2427                        d.resumable = false; 
     2428                        d.safeRetry(); 
     2429                        return false; 
     2430                } 
     2431                return false; 
    23022432        }, 
    23032433         
     
    23352465        }, 
    23362466         
    2337         //nsIRequestObserver, 
     2467        // nsIRequestObserver, 
    23382468        _supportedChannels: [ 
    23392469                {i:Ci.nsIHttpChannel, f:'handleHttp'}, 
     
    23522482                                try { 
    23532483                                        chan = aRequest.QueryInterface(sc.i); 
    2354                                 } 
    2355                                 catch (ex) { 
    2356                                         continue; 
    2357                                 } 
    2358                                 if (chan) { 
    23592484                                        if ((this.rexamine = this[sc.f](chan))) { 
    23602485                                                 return; 
    23612486                                        } 
    23622487                                        break; 
    2363                                 }                                        
     2488                                } 
     2489                                catch (ex) { 
     2490                                        // continue 
     2491                                } 
    23642492                        } 
    23652493 
     
    23692497                                if (d.fileName.getExtension() == 'metalink') { 
    23702498                                        d.isMetalink = true; 
    2371                                         d.resumable = true; 
     2499                                        d.resumable = false; 
    23722500                                }                                
    23732501                                 
     
    23882516                                                } 
    23892517                                                var nsd = Utils.getFreeDisk(realDest); 
    2390                                                 // Same save path or same disk (we assume that tmp.avail == dst.avail means same disk) 
     2518                                                // Same save path or same disk (we assume that tmp.avail == 
     2519                                                // dst.avail means same disk) 
    23912520                                                // simply moving should succeed 
    23922521                                                if (d.compression && (!tmp || Utils.getFreeDisk(vtmp) == nsd)) { 
    2393                                                         // we cannot know how much space we will consume after decompressing. 
    2394                                                         // so we assume factor 1.0 for the compressed and factor 1.5 for the decompressed file. 
     2522                                                        // we cannot know how much space we will consume after 
     2523                                                        // decompressing. 
     2524                                                        // so we assume factor 1.0 for the compressed and factor 1.5 for 
     2525                                                        // the decompressed file. 
    23952526                                                        tsd *= 2.5; 
    23962527                                                } 
     
    24742605                        d.markAutoRetry();                               
    24752606                        return; 
    2476                 }                
    2477  
     2607                } 
     2608                 
     2609                // work-around for ftp crap 
     2610                // nsiftpchan for some reason assumes that if RETR fails it is a directory 
     2611                // and tries to advance into said directory 
     2612                if (aStatusCode == NS_ERROR_FTP_CWD) { 
     2613                        Debug.logString("Cannot change to directory :p", aStatusCode); 
     2614                        d.cancel(); 
     2615                        return; 
     2616                } 
     2617                         
    24782618                // routine for normal chunk 
    24792619                Debug.logString(d + ": Chunk " + c.start + "-" + c.end + " finished."); 
    24802620                 
    2481                 // rude way to determine disconnection: if connection is closed before download is started we assume a server error/disconnection 
     2621                // rude way to determine disconnection: if connection is closed before 
     2622                // download is started we assume a server error/disconnection 
    24822623                if (c.starter && d.is(RUNNING)) { 
    24832624                        if (!d.urlManager.markBad(this.url)) { 
     
    26282769                } 
    26292770                else { 
    2630                         qi.hash = null; // to initialize prettyHash  
     2771                        qi.hash = null; // to initialize prettyHash 
    26312772                } 
    26322773 
  • trunk/chrome/locale/en-US/manager.properties

    r1031 r1037  
    6262loading=Loading downloads (%S of %S, %S%). Please wait... 
    6363adding=Adding downloads. Please wait... 
     64ftperrortext=Wasn't able to communicate with the FTP Server or the Server responded incorrectly