<commit_before>package org.klomp.snark.web;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.i2p.I2PAppContext;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;

import org.klomp.snark.MetaInfo;
import org.klomp.snark.Peer;
import org.klomp.snark.Snark;
import org.klomp.snark.SnarkManager;
import org.klomp.snark.Storage;
import org.klomp.snark.TrackerClient;

import org.mortbay.http.HttpResponse;
import org.mortbay.jetty.servlet.Default;
import org.mortbay.util.Resource;
import org.mortbay.util.URI;

/**
 *  We extend Default instead of HTTPServlet so we can handle
 *  i2psnark/ file requests with http:// instead of the flaky and
 *  often-blocked-by-the-browser file://
 */
public class I2PSnarkServlet extends Default {
    private I2PAppContext _context;
    private Log _log;
    private SnarkManager _manager;
    private static long _nonce;
    private Resource _resourceBase;
    
    public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
    
    @Override
    public void init(ServletConfig cfg) throws ServletException {
        _context = I2PAppContext.getGlobalContext();
        _log = _context.logManager().getLog(I2PSnarkServlet.class);
        _nonce = _context.random().nextLong();
        _manager = SnarkManager.instance();
        String configFile = _context.getProperty(PROP_CONFIG_FILE);
        if ( (configFile == null) || (configFile.trim().length() <= 0) )
            configFile = "i2psnark.config";
        _manager.loadConfig(configFile);
        _manager.start();
        try {
            _resourceBase = Resource.newResource(_manager.getDataDir().getAbsolutePath());
        } catch (IOException ioe) {}
        super.init(cfg);
    }
    
    @Override
    public void destroy() {
        _manager.stop();
        super.destroy();
    }

    /**
     *  We override this instead of passing a resource base to super(), because
     *  if a resource base is set, super.getResource() always uses that base,
     *  and we can't get any resources (like icons) out of the .war
     */
    @Override
    protected Resource getResource(String pathInContext) throws IOException
    {
        if (pathInContext == null || pathInContext.equals("/") || pathInContext.equals("/index.jsp") ||
            pathInContext.equals("/index.html") || pathInContext.startsWith("/_icons/"))
            return super.getResource(pathInContext);
        // files in the i2psnark/ directory
        return _resourceBase.addPath(pathInContext);
    }

    /**
     * Some parts modified from:
     * <pre>
      // ========================================================================
      // $Id: Default.java,v 1.51 2006/10/08 14:13:18 gregwilkins Exp $
      // Copyright 199-2004 Mort Bay Consulting Pty. Ltd.
      // ------------------------------------------------------------------------
      // Licensed under the Apache License, Version 2.0 (the "License");
      // you may not use this file except in compliance with the License.
      // You may obtain a copy of the License at 
      // http://www.apache.org/licenses/LICENSE-2.0
      // Unless required by applicable law or agreed to in writing, software
      // distributed under the License is distributed on an "AS IS" BASIS,
      // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      // See the License for the specific language governing permissions and
      // limitations under the License.
      // ========================================================================
     * </pre>
     *
     */
    @Override
    public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // since we are not overriding handle*(), do this here
        String method = req.getMethod();
        if (!(method.equals("GET") || method.equals("HEAD") || method.equals("POST"))) {
            resp.sendError(HttpResponse.__405_Method_Not_Allowed);
            return;
        }
        // this is the part after /i2psnark
        String path = req.getServletPath();
        boolean isConfigure = "/configure".equals(path);
        // index.jsp doesn't work, it is grabbed by the war handler before here
        if (!(path == null || path.equals("/") || path.equals("/index.jsp") || path.equals("/index.html") || isConfigure)) {
            if (path.endsWith("/")) {
                // bypass the horrid Resource.getListHTML()
                String pathInfo = req.getPathInfo();
                String pathInContext = URI.addPaths(path, pathInfo);
                resp.setCharacterEncoding("UTF-8");
                resp.setContentType("text/html; charset=UTF-8");
                Resource resource = getResource(pathInContext);
                if (resource == null || (!resource.exists()) || !resource.isDirectory()) {
                    resp.sendError(HttpResponse.__404_Not_Found);
                } else {
                    String base = URI.addPaths(req.getRequestURI(), "/");
                    String listing = getListHTML(resource, base, true);
                    if (listing != null)
                        resp.getWriter().write(listing);
                    else // shouldn't happen
                        resp.sendError(HttpResponse.__404_Not_Found);
                }
            } else {
                super.service(req, resp);
            }
            return;
        }

        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html; charset=UTF-8");
        
        String nonce = req.getParameter("nonce");
        if ( (nonce != null) && (nonce.equals(String.valueOf(_nonce))) )
            processRequest(req);
        
        String peerParam = req.getParameter("p");
        String peerString;
        if (peerParam == null) {
            peerString = "";
        } else {
            peerString = "?p=" + peerParam;
        }

        PrintWriter out = resp.getWriter();
        out.write("<html>\n" +
                  "<head>\n" +
                  "<title>");
        out.write(_("I2PSnark - Anonymous BitTorrent Client"));
        out.write("</title>\n");
                                         
        // we want it to go to the base URI so we don't refresh with some funky action= value
        if (!isConfigure)
            out.write("<meta http-equiv=\"refresh\" content=\"60;" + req.getRequestURI() + peerString + "\">\n");
        out.write(HEADER);
        out.write("</head><body>");
        out.write("<center>");
        if (isConfigure) {
            out.write("<div class=\"snarknavbar\"><a href=\"/i2psnark/\" title=\"");
            out.write(_("Torrents"));
            out.write("\" class=\"snarkRefresh\">");
            out.write(_("I2PSnark"));
            out.write("</a>");
        } else {
            out.write("<div class=\"snarknavbar\"><a href=\"" + req.getRequestURI() + peerString + "\" title=\"");
            out.write(_("Refresh page"));
            out.write("\" class=\"snarkRefresh\">");
            out.write(_("I2PSnark"));
            out.write("</a> <a href=\"http://forum.i2p/viewforum.php?f=21\" class=\"snarkRefresh\" target=\"_blank\">");
            out.write(_("Forum"));
            out.write("</a>\n");

            Map trackers = _manager.getTrackers();
            for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
                Map.Entry entry = (Map.Entry)iter.next();
                String name = (String)entry.getKey();
                String baseURL = (String)entry.getValue();
                int e = baseURL.indexOf('=');
                if (e < 0)
                    continue;
                baseURL = baseURL.substring(e + 1);
                out.write(" <a href=\"" + baseURL + "\" class=\"snarkRefresh\" target=\"_blank\">" + name + "</a>");
            }
        }
        out.write("</div>\n");
        out.write("<div class=\"page\"><div class=\"mainsection\"><div class=\"snarkMessages\"><table><tr><td align=\"left\"><pre>");
        List msgs = _manager.getMessages();
        for (int i = msgs.size()-1; i >= 0; i--) {
            String msg = (String)msgs.get(i);
            out.write(msg + "\n");
        }
        out.write("</pre></td></tr></table></div>");

        if (isConfigure) {
            out.write("</div>\n");
            writeConfigForm(out, req);
        } else {
            writeTorrents(out, req);
            out.write("</div>\n");
            writeAddForm(out, req);
            writeSeedForm(out, req);
            writeConfigLink(out);
        }
        out.write(FOOTER);
    }

    private void writeTorrents(PrintWriter out, HttpServletRequest req) throws IOException {
        /** dl, ul, down rate, up rate, peers, size */
        final long stats[] = {0,0,0,0,0,0};
        String peerParam = req.getParameter("p");

        List snarks = getSortedSnarks(req);
        String uri = req.getRequestURI();
        out.write(TABLE_HEADER);
        out.write(_("Status"));
        if (_manager.util().connected() && !snarks.isEmpty()) {
            out.write(" (<a href=\"");
            out.write(req.getRequestURI());
            if (peerParam != null) {
                out.write("\">");
                out.write(_("Hide Peers"));
            } else {
                out.write("?p=1\">");
                out.write(_("Show Peers"));
            }
            out.write("</a>)<br>\n");
        }
        out.write("</th>\n<th align=\"left\">");
        out.write(_("Torrent"));
        out.write("</th>\n<th align=\"center\">");
        out.write(_("ETA"));
        out.write("</th>\n<th align=\"right\">");
        out.write(_("Downloaded"));
        out.write("</th>\n<th align=\"right\">");
        out.write(_("Uploaded"));
        out.write("</th>\n<th align=\"right\">");
        out.write(_("Down Rate"));
        out.write("</th>\n<th align=\"right\">");
        out.write(_("Up Rate"));
        out.write("</th>\n");
    
        out.write("<th align=\"center\">");
        if (_manager.util().connected()) {
            out.write("<a href=\"" + uri + "?action=StopAll&nonce=" + _nonce +
                      "\" title=\"");
            out.write(_("Stop all torrents and the I2P tunnel"));
            out.write("\">");
            out.write(_("Stop All"));
            out.write("</a>");
        } else if (!snarks.isEmpty()) {
            out.write("<a href=\"" + uri + "?action=StartAll&nonce=" + _nonce +
                      "\" title=\"");
            out.write(_("Start all torrents and the I2P tunnel"));
            out.write("\">");
            out.write(_("Start All"));
            out.write("</a>");
        } else {
            out.write("&nbsp;");
        }
        out.write("</th></tr></thead>\n");
        for (int i = 0; i < snarks.size(); i++) {
            Snark snark = (Snark)snarks.get(i);
            boolean showDebug = "2".equals(peerParam);
            boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.meta.getInfoHash()).equals(peerParam);
            displaySnark(out, snark, uri, i, stats, showPeers, showDebug);
        }

        if (snarks.isEmpty()) {
            out.write("<tr class=\"snarkTorrentEven\">" +
                      "<td class=\"snarkTorrentEven\" align=\"center\"" +
                      " colspan=\"8\"><i>");
            out.write(_("No torrents loaded."));
            out.write("</i></td></tr>\n");
        } else if (snarks.size() > 1) {
            out.write("<tfoot><tr>\n" +
                      "    <th align=\"left\" colspan=\"2\">");
            out.write(_("Totals"));
            out.write(" (");
            out.write(ngettext("1 torrent", "{0} torrents", snarks.size()));
            out.write(", ");
            out.write(DataHelper.formatSize2(stats[5]) + "B, ");
            out.write(ngettext("1 connected peer", "{0} connected peers", (int) stats[4]));
            out.write(")</th>\n" +
                      "    <th>&nbsp;</th>\n" +
                      "    <th align=\"right\">" + formatSize(stats[0]) + "</th>\n" +
                      "    <th align=\"right\">" + formatSize(stats[1]) + "</th>\n" +
                      "    <th align=\"right\">" + formatSize(stats[2]) + "ps</th>\n" +
                      "    <th align=\"right\">" + formatSize(stats[3]) + "ps</th>\n" +
                      "    <th>&nbsp;</th></tr>\n" +
                      "</tfoot>\n");
        }
        
        out.write("</table>");
    }
    
    /**
     * Do what they ask, adding messages to _manager.addMessage as necessary
     */
    private void processRequest(HttpServletRequest req) {
        String action = req.getParameter("action");
        if (action == null) {
            // noop
        } else if ("Add".equals(action)) {
            String newFile = req.getParameter("newFile");
            String newURL = req.getParameter("newURL");
            // NOTE - newFile currently disabled in HTML form - see below
            File f = null;
            if ( (newFile != null) && (newFile.trim().length() > 0) )
                f = new File(newFile.trim());
            if ( (f != null) && (!f.exists()) ) {
                _manager.addMessage(_("Torrent file {0} does not exist", newFile));
            }
            if ( (f != null) && (f.exists()) ) {
                File local = new File(_manager.getDataDir(), f.getName());
                String canonical = null;
                try {
                    canonical = local.getCanonicalPath();
                    
                    if (local.exists()) {
                        if (_manager.getTorrent(canonical) != null)
                            _manager.addMessage(_("Torrent already running: {0}", newFile));
                        else
                            _manager.addMessage(_("Torrent already in the queue: {0}", newFile));
                    } else {
                        boolean ok = FileUtil.copy(f.getAbsolutePath(), local.getAbsolutePath(), true);
                        if (ok) {
                            _manager.addMessage(_("Copying torrent to {0}", local.getAbsolutePath()));
                            _manager.addTorrent(canonical);
                        } else {
                            _manager.addMessage(_("Unable to copy the torrent to {0}", local.getAbsolutePath()) + ' ' + _("from {0}", f.getAbsolutePath()));
                        }
                    }
                } catch (IOException ioe) {
                    _log.warn("hrm: " + local, ioe);
                }
            } else if (newURL != null) {
                if (newURL.startsWith("http://")) {
                    _manager.addMessage(_("Fetching {0}", urlify(newURL)));
                    I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add");
                    fetch.start();
                } else {
                    _manager.addMessage(_("Invalid URL - must start with http://"));
                }
            } else {
                // no file or URL specified
            }
        } else if ("Stop".equals(action)) {
            String torrent = req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            _manager.stopTorrent(name, false);
                            break;
                        }
                    }
                }
            }
        } else if ("Start".equals(action)) {
            String torrent = req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            snark.startTorrent();
                            _manager.addMessage(_("Starting up torrent {0}", name));
                            break;
                        }
                    }
                }
            }
        } else if ("Remove".equals(action)) {
            String torrent = req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            _manager.stopTorrent(name, true);
                            // should we delete the torrent file?
                            // yeah, need to, otherwise it'll get autoadded again (at the moment
                            File f = new File(name);
                            f.delete();
                            _manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath()));
                            break;
                        }
                    }
                }
            }
        } else if ("Delete".equals(action)) {
            String torrent = req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            _manager.stopTorrent(name, true);
                            File f = new File(name);
                            f.delete();
                            _manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath()));
                            List files = snark.meta.getFiles();
                            String dataFile = snark.meta.getName();
                            f = new File(_manager.getDataDir(), dataFile);
                            if (files == null) { // single file torrent
                                if (f.delete())
                                    _manager.addMessage(_("Data file deleted: {0}", f.getAbsolutePath()));
                                else
                                    _manager.addMessage(_("Data file could not be deleted: {0}", f.getAbsolutePath()));
                                break;
                            }
                            for (int i = 0; i < files.size(); i++) { // pass 1 delete files
                                // multifile torrents have the getFiles() return lists of lists of filenames, but
                                // each of those lists just contain a single file afaict...
                                File df = Storage.getFileFromNames(f, (List) files.get(i));
                                if (df.delete())
                                    _manager.addMessage(_("Data file deleted: {0}", df.getAbsolutePath()));
                                else
                                    _manager.addMessage(_("Data file could not be deleted: {0}", df.getAbsolutePath()));
                            }
                            for (int i = files.size() - 1; i >= 0; i--) { // pass 2 delete dirs - not foolproof,
                                                                          // we could sort and do a strict bottom-up
                                File df = Storage.getFileFromNames(f, (List) files.get(i));
                                df = df.getParentFile();
                                if (df == null || !df.exists())
                                    continue;
                                if(df.delete())
                                    _manager.addMessage(_("Data dir deleted: {0}", df.getAbsolutePath()));
                            }
                            break;
                        }
                    }
                }
            }
        } else if ("Save".equals(action)) {
            String dataDir = req.getParameter("dataDir");
            boolean autoStart = req.getParameter("autoStart") != null;
            String seedPct = req.getParameter("seedPct");
            String eepHost = req.getParameter("eepHost");
            String eepPort = req.getParameter("eepPort");
            String i2cpHost = req.getParameter("i2cpHost");
            String i2cpPort = req.getParameter("i2cpPort");
            String i2cpOpts = buildI2CPOpts(req);
            String upLimit = req.getParameter("upLimit");
            String upBW = req.getParameter("upBW");
	    String startupDel = req.getParameter("startupDelay");
            boolean useOpenTrackers = req.getParameter("useOpenTrackers") != null;
            String openTrackers = req.getParameter("openTrackers");
            _manager.updateConfig(dataDir, autoStart, startupDel, seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts, upLimit, upBW, useOpenTrackers, openTrackers);
        } else if ("Create".equals(action)) {
            String baseData = req.getParameter("baseFile");
            if (baseData != null && baseData.trim().length() > 0) {
                File baseFile = new File(_manager.getDataDir(), baseData);
                String announceURL = req.getParameter("announceURL");
                String announceURLOther = req.getParameter("announceURLOther");
                if ( (announceURLOther != null) && (announceURLOther.trim().length() > "http://.i2p/announce".length()) )
                    announceURL = announceURLOther;

                if (announceURL == null || announceURL.length() <= 0)
                    _manager.addMessage(_("Error creating torrent - you must select a tracker"));
                else if (baseFile.exists()) {
                    try {
                        Storage s = new Storage(_manager.util(), baseFile, announceURL, null);
                        s.create();
                        s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
                        MetaInfo info = s.getMetaInfo();
                        File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent");
                        if (torrentFile.exists())
                            throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath());
                        _manager.saveTorrentStatus(info, s.getBitField()); // so addTorrent won't recheck
                        // DirMonitor could grab this first, maybe hold _snarks lock?
                        FileOutputStream out = new FileOutputStream(torrentFile);
                        out.write(info.getTorrentData());
                        out.close();
                        _manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
                        // now fire it up, but don't automatically seed it
                        _manager.addTorrent(torrentFile.getCanonicalPath(), true);
                        _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
                    } catch (IOException ioe) {
                        _manager.addMessage(_("Error creating a torrent for \"{0}\"", baseFile.getAbsolutePath()) + ": " + ioe.getMessage());
                    }
                } else {
                    _manager.addMessage(_("Cannot create a torrent for the nonexistent data: {0}", baseFile.getAbsolutePath()));
                }
            } else {
                _manager.addMessage(_("Error creating torrent - you must enter a file or directory"));
            }
        } else if ("StopAll".equals(action)) {
            _manager.addMessage(_("Stopping all torrents and closing the I2P tunnel."));
            List snarks = getSortedSnarks(req);
            for (int i = 0; i < snarks.size(); i++) {
                Snark snark = (Snark)snarks.get(i);
                if (!snark.stopped)
                    _manager.stopTorrent(snark.torrent, false);
            }
            if (_manager.util().connected()) {
                _manager.util().disconnect();
                _manager.addMessage(_("I2P tunnel closed."));
            }
        } else if ("StartAll".equals(action)) {
            _manager.addMessage(_("Opening the I2P tunnel and starting all torrents."));
            List snarks = getSortedSnarks(req);
            for (int i = 0; i < snarks.size(); i++) {
                Snark snark = (Snark)snarks.get(i);
                if (snark.stopped)
                    snark.startTorrent();
            }
        }
    }
    
    private static final String iopts[] = {"inbound.length", "inbound.quantity",
                                           "outbound.length", "outbound.quantity" };

    /** put the individual i2cp selections into the option string */
    private static String buildI2CPOpts(HttpServletRequest req) {
        StringBuilder buf = new StringBuilder(128);
        String p = req.getParameter("i2cpOpts");
        if (p != null)
            buf.append(p);
        for (int i = 0; i < iopts.length; i++) {
            p = req.getParameter(iopts[i]);
            if (p != null)
                buf.append(' ').append(iopts[i]).append('=').append(p);
        }
        return buf.toString();
    }

    /**
     *  Sort alphabetically in current locale, ignore case, ignore leading "the "
     *  (I guess this is worth it, a lot of torrents start with "The "
     *  These are full path names which makes it harder
     *  @since 0.7.14
     */
    private class TorrentNameComparator implements Comparator<String> {
        private final Comparator collator = Collator.getInstance();
        private final String skip = _manager.getDataDir().getAbsolutePath() + File.separator;

        public int compare(String l, String r) {
            if (l.startsWith(skip))
                l = l.substring(skip.length());
            if (r.startsWith(skip))
                r = r.substring(skip.length());
            String llc = l.toLowerCase();
            if (llc.startsWith("the ") || llc.startsWith("the."))
                l = l.substring(4);
            String rlc = r.toLowerCase();
            if (rlc.startsWith("the ") || rlc.startsWith("the."))
                r = r.substring(4);
            return collator.compare(l, r);
        }
    }

    private List<Snark> getSortedSnarks(HttpServletRequest req) {
        Set<String> files = _manager.listTorrentFiles();
        TreeSet<String> fileNames = new TreeSet(new TorrentNameComparator());
        fileNames.addAll(files);
        ArrayList<Snark> rv = new ArrayList(fileNames.size());
        for (Iterator iter = fileNames.iterator(); iter.hasNext(); ) {
            String name = (String)iter.next();
            Snark snark = _manager.getTorrent(name);
            if (snark != null)
                rv.add(snark);
        }
        return rv;
    }

    private static final int MAX_DISPLAYED_FILENAME_LENGTH = 44;
    private static final int MAX_DISPLAYED_ERROR_LENGTH = 40;
    private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers, boolean showDebug) throws IOException {
        String filename = snark.torrent;
        File f = new File(filename);
        filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name
        int i = filename.lastIndexOf(".torrent");
        if (i > 0)
            filename = filename.substring(0, i);
        String fullFilename = filename;
        if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH) {
            fullFilename = new String(filename);
            filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "&hellip;";
        }
        long total = snark.meta.getTotalLength();
        // Early typecast, avoid possibly overflowing a temp integer
        long remaining = (long) snark.storage.needed() * (long) snark.meta.getPieceLength(0); 
        if (remaining > total)
            remaining = total;
        long downBps = 0;
        long upBps = 0;
        if (snark.coordinator != null) {
            downBps = snark.coordinator.getDownloadRate();
            upBps = snark.coordinator.getUploadRate();
        }
        long remainingSeconds;
        if (downBps > 0)
            remainingSeconds = remaining / downBps;
        else
            remainingSeconds = -1;
        boolean isRunning = !snark.stopped;
        long uploaded = 0;
        if (snark.coordinator != null) {
            uploaded = snark.coordinator.getUploaded();
            stats[0] += snark.coordinator.getDownloaded();
        }
        stats[1] += uploaded;
        if (isRunning) {
            stats[2] += downBps;
            stats[3] += upBps;
        }
        stats[5] += total;
        
        boolean isValid = snark.meta != null;
        boolean singleFile = snark.meta.getFiles() == null;
        
        String err = null;
        int curPeers = 0;
        int knownPeers = 0;
        if (snark.coordinator != null) {
            err = snark.coordinator.trackerProblems;
            curPeers = snark.coordinator.getPeerCount();
            stats[4] += curPeers;
            knownPeers = Math.max(curPeers, snark.coordinator.trackerSeenPeers);
        }
        
        String statusString = _("Unknown");
        if (err != null) {
            if (isRunning && curPeers > 0 && !showPeers)
                statusString = "<a title=\"" + err + "\">" + _("TrackerErr") + "</a> (" +
                               "<a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
                               curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + "</a>)";
            else if (isRunning)
                statusString = "<a title=\"" + err + "\">" + _("TrackerErr") + " (" + curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + ')';
            else {
                if (err.length() > MAX_DISPLAYED_ERROR_LENGTH)
                    err = err.substring(0, MAX_DISPLAYED_ERROR_LENGTH) + "&hellip;";
                statusString = _("TrackerErr") + "<br>(" + err + ")";
            }
        } else if (remaining <= 0) {
            if (isRunning && curPeers > 0 && !showPeers)
                statusString = _("Seeding") + " (" +
                               "<a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
                               curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + "</a>)";
            else if (isRunning)
                statusString = _("Seeding") + " (" + curPeers + "/" +
                               ngettext("1 peer", "{0} peers", knownPeers) + ')';
            else
                statusString = _("Complete");
        } else {
            if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
                statusString = _("OK") + " (" +
                               "<a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
                               curPeers + "/" +
                               ngettext("1 peer", "{0} peers", knownPeers) + "</a>)";
            else if (isRunning && curPeers > 0 && downBps > 0)
                statusString = _("OK") + " (" + curPeers + "/" +
                               ngettext("1 peer", "{0} peers", knownPeers) + ')';
            else if (isRunning && curPeers > 0 && !showPeers)
                statusString = _("Stalled") + " (" +
                               "<a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
                               curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + "</a>)";
            else if (isRunning && curPeers > 0)
                statusString = _("Stalled") + " (" + curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + ')';
            else if (isRunning)
                statusString = _("No Peers") + " (0/" + knownPeers + ')';
            else
                statusString = _("Stopped");
        }
        
        String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
        out.write("<tr class=\"" + rowClass + "\">");
        out.write("<td align=\"center\" class=\"snarkTorrentStatus " + rowClass + "\">");
        out.write(statusString + "</td>\n\t");
        out.write("<td align=\"left\" class=\"snarkTorrentName " + rowClass + "\">");
        
        if (remaining == 0 || snark.meta.getFiles() != null) {
            out.write("<a href=\"" + snark.storage.getBaseName());
            if (snark.meta.getFiles() != null)
                out.write("/");
            out.write("\" title=\"");
            if (snark.meta.getFiles() != null)
                out.write(_("View files"));
            else
                out.write(_("Open file"));
            out.write("\">");
        }
        String icon;
        if (snark.meta.getFiles() != null)
            icon = "folder";
        else
            icon = toIcon(snark.meta.getName());
        out.write(toImg(icon));
        out.write(filename);
        if (remaining == 0 || snark.meta.getFiles() != null)
            out.write("</a>");
        // temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash
        String announce = snark.meta.getAnnounce();
        if (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") ||
            announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/")) {
            Map trackers = _manager.getTrackers();
            for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
                Map.Entry entry = (Map.Entry)iter.next();
                String name = (String)entry.getKey();
                String baseURL = (String)entry.getValue();
                if (!(baseURL.startsWith(announce) || // vvv hack for non-b64 announce in list vvv
                      (announce.startsWith("http://lnQ6yoBT") && baseURL.startsWith("http://tracker2.postman.i2p/"))))
                    continue;
                int e = baseURL.indexOf('=');
                if (e < 0)
                    continue;
                baseURL = baseURL.substring(e + 1);
                out.write("&nbsp;&nbsp;&nbsp;[<a href=\"" + baseURL + "details.php?dllist=1&filelist=1&info_hash=");
                out.write(TrackerClient.urlencode(snark.meta.getInfoHash()));
                out.write("\" title=\"" + name + ' ' + _("Tracker") + "\">");
                out.write(_("Details"));
                out.write("</a>]");
                break;
            }
        }
        out.write("</td>\n\t");
        
        out.write("<td align=\"center\" class=\"snarkTorrentETA " + rowClass + "\">");
        if(isRunning && remainingSeconds > 0)
            out.write(DataHelper.formatDuration(remainingSeconds*1000)); // (eta 6h)
        out.write("</td>\n\t");
        out.write("<td align=\"right\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
        if (remaining > 0)
            out.write(formatSize(total-remaining) + " / " + formatSize(total)); // 18MB/3GB
        else
            out.write(formatSize(total)); // 3GB
        out.write("</td>\n\t");
        out.write("<td align=\"right\" class=\"snarkTorrentUploaded " + rowClass 
                  + "\">" + formatSize(uploaded) + "</td>\n\t");
        out.write("<td align=\"right\" class=\"snarkTorrentRate\">");
        if(isRunning && remaining > 0)
            out.write(formatSize(downBps) + "ps");
        out.write("</td>\n\t");
        out.write("<td align=\"right\" class=\"snarkTorrentRate\">");
        if(isRunning)
            out.write(formatSize(upBps) + "ps");
        out.write("</td>\n\t");
        out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">");
        String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.meta.getInfoHash());
        if (showPeers)
            parameters = parameters + "&p=1";
        if (isRunning) {
            out.write("<a href=\"" + uri + "?action=Stop" + parameters
                      + "\" title=\"");
            out.write(_("Stop the torrent"));
            out.write("\">");
            out.write(_("Stop"));
            out.write("</a>");
        } else {
            if (isValid) {
                out.write("<a href=\"" + uri + "?action=Start" + parameters
                          + "\" title=\"");
                out.write(_("Start the torrent"));
                out.write("\">");
                out.write(_("Start"));
                out.write("</a>\n");
            }
            out.write("<a href=\"" + uri + "?action=Remove" + parameters
                      + "\" title=\"");
            out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
            out.write("\" onclick=\"if (!confirm('");
            // Can't figure out how to escape double quotes inside the onclick string.
            // Single quotes in translate strings with parameters must be doubled.
            // Then the remaining single quite must be escaped
            out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
            out.write("')) { return false; }\">");
            out.write(_("Remove"));
            out.write("</a><br>");
            out.write("<a href=\"" + uri + "?action=Delete" + parameters
                      + "\" title=\"");
            out.write(_("Delete the .torrent file and the associated data file(s)"));
            out.write("\" onclick=\"if (!confirm('");
            // Can't figure out how to escape double quotes inside the onclick string.
            // Single quotes in translate strings with parameters must be doubled.
            // Then the remaining single quite must be escaped
            out.write(_("Are you sure you want to delete the torrent \\''{0}\\'' and all downloaded data?", fullFilename));
            out.write("')) { return false; }\">");
            out.write(_("Delete"));
            out.write("</a>");
        }
        out.write("</td>\n</tr>\n");

        if(showPeers && isRunning && curPeers > 0) {
            List<Peer> peers = snark.coordinator.peerList();
            Collections.sort(peers, new PeerComparator());
            for (Peer peer : peers) {
                if (!peer.isConnected())
                    continue;
                out.write("<tr class=\"" + rowClass + "\">");
                out.write("<td align=\"center\" class=\"snarkTorrentStatus " + rowClass + "\">");
                out.write("</td>\n\t");
                out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");<commit_msg>dont sort in debug mode<commit_after>package org.klomp.snark.web;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.i2p.I2PAppContext;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;

import org.klomp.snark.MetaInfo;
import org.klomp.snark.Peer;
import org.klomp.snark.Snark;
import org.klomp.snark.SnarkManager;
import org.klomp.snark.Storage;
import org.klomp.snark.TrackerClient;

import org.mortbay.http.HttpResponse;
import org.mortbay.jetty.servlet.Default;
import org.mortbay.util.Resource;
import org.mortbay.util.URI;

/**
 *  We extend Default instead of HTTPServlet so we can handle
 *  i2psnark/ file requests with http:// instead of the flaky and
 *  often-blocked-by-the-browser file://
 */
public class I2PSnarkServlet extends Default {
    private I2PAppContext _context;
    private Log _log;
    private SnarkManager _manager;
    private static long _nonce;
    private Resource _resourceBase;
    
    public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
    
    @Override
    public void init(ServletConfig cfg) throws ServletException {
        _context = I2PAppContext.getGlobalContext();
        _log = _context.logManager().getLog(I2PSnarkServlet.class);
        _nonce = _context.random().nextLong();
        _manager = SnarkManager.instance();
        String configFile = _context.getProperty(PROP_CONFIG_FILE);
        if ( (configFile == null) || (configFile.trim().length() <= 0) )
            configFile = "i2psnark.config";
        _manager.loadConfig(configFile);
        _manager.start();
        try {
            _resourceBase = Resource.newResource(_manager.getDataDir().getAbsolutePath());
        } catch (IOException ioe) {}
        super.init(cfg);
    }
    
    @Override
    public void destroy() {
        _manager.stop();
        super.destroy();
    }

    /**
     *  We override this instead of passing a resource base to super(), because
     *  if a resource base is set, super.getResource() always uses that base,
     *  and we can't get any resources (like icons) out of the .war
     */
    @Override
    protected Resource getResource(String pathInContext) throws IOException
    {
        if (pathInContext == null || pathInContext.equals("/") || pathInContext.equals("/index.jsp") ||
            pathInContext.equals("/index.html") || pathInContext.startsWith("/_icons/"))
            return super.getResource(pathInContext);
        // files in the i2psnark/ directory
        return _resourceBase.addPath(pathInContext);
    }

    /**
     * Some parts modified from:
     * <pre>
      // ========================================================================
      // $Id: Default.java,v 1.51 2006/10/08 14:13:18 gregwilkins Exp $
      // Copyright 199-2004 Mort Bay Consulting Pty. Ltd.
      // ------------------------------------------------------------------------
      // Licensed under the Apache License, Version 2.0 (the "License");
      // you may not use this file except in compliance with the License.
      // You may obtain a copy of the License at 
      // http://www.apache.org/licenses/LICENSE-2.0
      // Unless required by applicable law or agreed to in writing, software
      // distributed under the License is distributed on an "AS IS" BASIS,
      // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      // See the License for the specific language governing permissions and
      // limitations under the License.
      // ========================================================================
     * </pre>
     *
     */
    @Override
    public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // since we are not overriding handle*(), do this here
        String method = req.getMethod();
        if (!(method.equals("GET") || method.equals("HEAD") || method.equals("POST"))) {
            resp.sendError(HttpResponse.__405_Method_Not_Allowed);
            return;
        }
        // this is the part after /i2psnark
        String path = req.getServletPath();
        boolean isConfigure = "/configure".equals(path);
        // index.jsp doesn't work, it is grabbed by the war handler before here
        if (!(path == null || path.equals("/") || path.equals("/index.jsp") || path.equals("/index.html") || isConfigure)) {
            if (path.endsWith("/")) {
                // bypass the horrid Resource.getListHTML()
                String pathInfo = req.getPathInfo();
                String pathInContext = URI.addPaths(path, pathInfo);
                resp.setCharacterEncoding("UTF-8");
                resp.setContentType("text/html; charset=UTF-8");
                Resource resource = getResource(pathInContext);
                if (resource == null || (!resource.exists()) || !resource.isDirectory()) {
                    resp.sendError(HttpResponse.__404_Not_Found);
                } else {
                    String base = URI.addPaths(req.getRequestURI(), "/");
                    String listing = getListHTML(resource, base, true);
                    if (listing != null)
                        resp.getWriter().write(listing);
                    else // shouldn't happen
                        resp.sendError(HttpResponse.__404_Not_Found);
                }
            } else {
                super.service(req, resp);
            }
            return;
        }

        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html; charset=UTF-8");
        
        String nonce = req.getParameter("nonce");
        if ( (nonce != null) && (nonce.equals(String.valueOf(_nonce))) )
            processRequest(req);
        
        String peerParam = req.getParameter("p");
        String peerString;
        if (peerParam == null) {
            peerString = "";
        } else {
            peerString = "?p=" + peerParam;
        }

        PrintWriter out = resp.getWriter();
        out.write("<html>\n" +
                  "<head>\n" +
                  "<title>");
        out.write(_("I2PSnark - Anonymous BitTorrent Client"));
        out.write("</title>\n");
                                         
        // we want it to go to the base URI so we don't refresh with some funky action= value
        if (!isConfigure)
            out.write("<meta http-equiv=\"refresh\" content=\"60;" + req.getRequestURI() + peerString + "\">\n");
        out.write(HEADER);
        out.write("</head><body>");
        out.write("<center>");
        if (isConfigure) {
            out.write("<div class=\"snarknavbar\"><a href=\"/i2psnark/\" title=\"");
            out.write(_("Torrents"));
            out.write("\" class=\"snarkRefresh\">");
            out.write(_("I2PSnark"));
            out.write("</a>");
        } else {
            out.write("<div class=\"snarknavbar\"><a href=\"" + req.getRequestURI() + peerString + "\" title=\"");
            out.write(_("Refresh page"));
            out.write("\" class=\"snarkRefresh\">");
            out.write(_("I2PSnark"));
            out.write("</a> <a href=\"http://forum.i2p/viewforum.php?f=21\" class=\"snarkRefresh\" target=\"_blank\">");
            out.write(_("Forum"));
            out.write("</a>\n");

            Map trackers = _manager.getTrackers();
            for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
                Map.Entry entry = (Map.Entry)iter.next();
                String name = (String)entry.getKey();
                String baseURL = (String)entry.getValue();
                int e = baseURL.indexOf('=');
                if (e < 0)
                    continue;
                baseURL = baseURL.substring(e + 1);
                out.write(" <a href=\"" + baseURL + "\" class=\"snarkRefresh\" target=\"_blank\">" + name + "</a>");
            }
        }
        out.write("</div>\n");
        out.write("<div class=\"page\"><div class=\"mainsection\"><div class=\"snarkMessages\"><table><tr><td align=\"left\"><pre>");
        List msgs = _manager.getMessages();
        for (int i = msgs.size()-1; i >= 0; i--) {
            String msg = (String)msgs.get(i);
            out.write(msg + "\n");
        }
        out.write("</pre></td></tr></table></div>");

        if (isConfigure) {
            out.write("</div>\n");
            writeConfigForm(out, req);
        } else {
            writeTorrents(out, req);
            out.write("</div>\n");
            writeAddForm(out, req);
            writeSeedForm(out, req);
            writeConfigLink(out);
        }
        out.write(FOOTER);
    }

    private void writeTorrents(PrintWriter out, HttpServletRequest req) throws IOException {
        /** dl, ul, down rate, up rate, peers, size */
        final long stats[] = {0,0,0,0,0,0};
        String peerParam = req.getParameter("p");

        List snarks = getSortedSnarks(req);
        String uri = req.getRequestURI();
        out.write(TABLE_HEADER);
        out.write(_("Status"));
        if (_manager.util().connected() && !snarks.isEmpty()) {
            out.write(" (<a href=\"");
            out.write(req.getRequestURI());
            if (peerParam != null) {
                out.write("\">");
                out.write(_("Hide Peers"));
            } else {
                out.write("?p=1\">");
                out.write(_("Show Peers"));
            }
            out.write("</a>)<br>\n");
        }
        out.write("</th>\n<th align=\"left\">");
        out.write(_("Torrent"));
        out.write("</th>\n<th align=\"center\">");
        out.write(_("ETA"));
        out.write("</th>\n<th align=\"right\">");
        out.write(_("Downloaded"));
        out.write("</th>\n<th align=\"right\">");
        out.write(_("Uploaded"));
        out.write("</th>\n<th align=\"right\">");
        out.write(_("Down Rate"));
        out.write("</th>\n<th align=\"right\">");
        out.write(_("Up Rate"));
        out.write("</th>\n");
    
        out.write("<th align=\"center\">");
        if (_manager.util().connected()) {
            out.write("<a href=\"" + uri + "?action=StopAll&nonce=" + _nonce +
                      "\" title=\"");
            out.write(_("Stop all torrents and the I2P tunnel"));
            out.write("\">");
            out.write(_("Stop All"));
            out.write("</a>");
        } else if (!snarks.isEmpty()) {
            out.write("<a href=\"" + uri + "?action=StartAll&nonce=" + _nonce +
                      "\" title=\"");
            out.write(_("Start all torrents and the I2P tunnel"));
            out.write("\">");
            out.write(_("Start All"));
            out.write("</a>");
        } else {
            out.write("&nbsp;");
        }
        out.write("</th></tr></thead>\n");
        for (int i = 0; i < snarks.size(); i++) {
            Snark snark = (Snark)snarks.get(i);
            boolean showDebug = "2".equals(peerParam);
            boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.meta.getInfoHash()).equals(peerParam);
            displaySnark(out, snark, uri, i, stats, showPeers, showDebug);
        }

        if (snarks.isEmpty()) {
            out.write("<tr class=\"snarkTorrentEven\">" +
                      "<td class=\"snarkTorrentEven\" align=\"center\"" +
                      " colspan=\"8\"><i>");
            out.write(_("No torrents loaded."));
            out.write("</i></td></tr>\n");
        } else if (snarks.size() > 1) {
            out.write("<tfoot><tr>\n" +
                      "    <th align=\"left\" colspan=\"2\">");
            out.write(_("Totals"));
            out.write(" (");
            out.write(ngettext("1 torrent", "{0} torrents", snarks.size()));
            out.write(", ");
            out.write(DataHelper.formatSize2(stats[5]) + "B, ");
            out.write(ngettext("1 connected peer", "{0} connected peers", (int) stats[4]));
            out.write(")</th>\n" +
                      "    <th>&nbsp;</th>\n" +
                      "    <th align=\"right\">" + formatSize(stats[0]) + "</th>\n" +
                      "    <th align=\"right\">" + formatSize(stats[1]) + "</th>\n" +
                      "    <th align=\"right\">" + formatSize(stats[2]) + "ps</th>\n" +
                      "    <th align=\"right\">" + formatSize(stats[3]) + "ps</th>\n" +
                      "    <th>&nbsp;</th></tr>\n" +
                      "</tfoot>\n");
        }
        
        out.write("</table>");
    }
    
    /**
     * Do what they ask, adding messages to _manager.addMessage as necessary
     */
    private void processRequest(HttpServletRequest req) {
        String action = req.getParameter("action");
        if (action == null) {
            // noop
        } else if ("Add".equals(action)) {
            String newFile = req.getParameter("newFile");
            String newURL = req.getParameter("newURL");
            // NOTE - newFile currently disabled in HTML form - see below
            File f = null;
            if ( (newFile != null) && (newFile.trim().length() > 0) )
                f = new File(newFile.trim());
            if ( (f != null) && (!f.exists()) ) {
                _manager.addMessage(_("Torrent file {0} does not exist", newFile));
            }
            if ( (f != null) && (f.exists()) ) {
                File local = new File(_manager.getDataDir(), f.getName());
                String canonical = null;
                try {
                    canonical = local.getCanonicalPath();
                    
                    if (local.exists()) {
                        if (_manager.getTorrent(canonical) != null)
                            _manager.addMessage(_("Torrent already running: {0}", newFile));
                        else
                            _manager.addMessage(_("Torrent already in the queue: {0}", newFile));
                    } else {
                        boolean ok = FileUtil.copy(f.getAbsolutePath(), local.getAbsolutePath(), true);
                        if (ok) {
                            _manager.addMessage(_("Copying torrent to {0}", local.getAbsolutePath()));
                            _manager.addTorrent(canonical);
                        } else {
                            _manager.addMessage(_("Unable to copy the torrent to {0}", local.getAbsolutePath()) + ' ' + _("from {0}", f.getAbsolutePath()));
                        }
                    }
                } catch (IOException ioe) {
                    _log.warn("hrm: " + local, ioe);
                }
            } else if (newURL != null) {
                if (newURL.startsWith("http://")) {
                    _manager.addMessage(_("Fetching {0}", urlify(newURL)));
                    I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add");
                    fetch.start();
                } else {
                    _manager.addMessage(_("Invalid URL - must start with http://"));
                }
            } else {
                // no file or URL specified
            }
        } else if ("Stop".equals(action)) {
            String torrent = req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            _manager.stopTorrent(name, false);
                            break;
                        }
                    }
                }
            }
        } else if ("Start".equals(action)) {
            String torrent = req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            snark.startTorrent();
                            _manager.addMessage(_("Starting up torrent {0}", name));
                            break;
                        }
                    }
                }
            }
        } else if ("Remove".equals(action)) {
            String torrent = req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            _manager.stopTorrent(name, true);
                            // should we delete the torrent file?
                            // yeah, need to, otherwise it'll get autoadded again (at the moment
                            File f = new File(name);
                            f.delete();
                            _manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath()));
                            break;
                        }
                    }
                }
            }
        } else if ("Delete".equals(action)) {
            String torrent = req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            _manager.stopTorrent(name, true);
                            File f = new File(name);
                            f.delete();
                            _manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath()));
                            List files = snark.meta.getFiles();
                            String dataFile = snark.meta.getName();
                            f = new File(_manager.getDataDir(), dataFile);
                            if (files == null) { // single file torrent
                                if (f.delete())
                                    _manager.addMessage(_("Data file deleted: {0}", f.getAbsolutePath()));
                                else
                                    _manager.addMessage(_("Data file could not be deleted: {0}", f.getAbsolutePath()));
                                break;
                            }
                            for (int i = 0; i < files.size(); i++) { // pass 1 delete files
                                // multifile torrents have the getFiles() return lists of lists of filenames, but
                                // each of those lists just contain a single file afaict...
                                File df = Storage.getFileFromNames(f, (List) files.get(i));
                                if (df.delete())
                                    _manager.addMessage(_("Data file deleted: {0}", df.getAbsolutePath()));
                                else
                                    _manager.addMessage(_("Data file could not be deleted: {0}", df.getAbsolutePath()));
                            }
                            for (int i = files.size() - 1; i >= 0; i--) { // pass 2 delete dirs - not foolproof,
                                                                          // we could sort and do a strict bottom-up
                                File df = Storage.getFileFromNames(f, (List) files.get(i));
                                df = df.getParentFile();
                                if (df == null || !df.exists())
                                    continue;
                                if(df.delete())
                                    _manager.addMessage(_("Data dir deleted: {0}", df.getAbsolutePath()));
                            }
                            break;
                        }
                    }
                }
            }
        } else if ("Save".equals(action)) {
            String dataDir = req.getParameter("dataDir");
            boolean autoStart = req.getParameter("autoStart") != null;
            String seedPct = req.getParameter("seedPct");
            String eepHost = req.getParameter("eepHost");
            String eepPort = req.getParameter("eepPort");
            String i2cpHost = req.getParameter("i2cpHost");
            String i2cpPort = req.getParameter("i2cpPort");
            String i2cpOpts = buildI2CPOpts(req);
            String upLimit = req.getParameter("upLimit");
            String upBW = req.getParameter("upBW");
	    String startupDel = req.getParameter("startupDelay");
            boolean useOpenTrackers = req.getParameter("useOpenTrackers") != null;
            String openTrackers = req.getParameter("openTrackers");
            _manager.updateConfig(dataDir, autoStart, startupDel, seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts, upLimit, upBW, useOpenTrackers, openTrackers);
        } else if ("Create".equals(action)) {
            String baseData = req.getParameter("baseFile");
            if (baseData != null && baseData.trim().length() > 0) {
                File baseFile = new File(_manager.getDataDir(), baseData);
                String announceURL = req.getParameter("announceURL");
                String announceURLOther = req.getParameter("announceURLOther");
                if ( (announceURLOther != null) && (announceURLOther.trim().length() > "http://.i2p/announce".length()) )
                    announceURL = announceURLOther;

                if (announceURL == null || announceURL.length() <= 0)
                    _manager.addMessage(_("Error creating torrent - you must select a tracker"));
                else if (baseFile.exists()) {
                    try {
                        Storage s = new Storage(_manager.util(), baseFile, announceURL, null);
                        s.create();
                        s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
                        MetaInfo info = s.getMetaInfo();
                        File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent");
                        if (torrentFile.exists())
                            throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath());
                        _manager.saveTorrentStatus(info, s.getBitField()); // so addTorrent won't recheck
                        // DirMonitor could grab this first, maybe hold _snarks lock?
                        FileOutputStream out = new FileOutputStream(torrentFile);
                        out.write(info.getTorrentData());
                        out.close();
                        _manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
                        // now fire it up, but don't automatically seed it
                        _manager.addTorrent(torrentFile.getCanonicalPath(), true);
                        _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
                    } catch (IOException ioe) {
                        _manager.addMessage(_("Error creating a torrent for \"{0}\"", baseFile.getAbsolutePath()) + ": " + ioe.getMessage());
                    }
                } else {
                    _manager.addMessage(_("Cannot create a torrent for the nonexistent data: {0}", baseFile.getAbsolutePath()));
                }
            } else {
                _manager.addMessage(_("Error creating torrent - you must enter a file or directory"));
            }
        } else if ("StopAll".equals(action)) {
            _manager.addMessage(_("Stopping all torrents and closing the I2P tunnel."));
            List snarks = getSortedSnarks(req);
            for (int i = 0; i < snarks.size(); i++) {
                Snark snark = (Snark)snarks.get(i);
                if (!snark.stopped)
                    _manager.stopTorrent(snark.torrent, false);
            }
            if (_manager.util().connected()) {
                _manager.util().disconnect();
                _manager.addMessage(_("I2P tunnel closed."));
            }
        } else if ("StartAll".equals(action)) {
            _manager.addMessage(_("Opening the I2P tunnel and starting all torrents."));
            List snarks = getSortedSnarks(req);
            for (int i = 0; i < snarks.size(); i++) {
                Snark snark = (Snark)snarks.get(i);
                if (snark.stopped)
                    snark.startTorrent();
            }
        }
    }
    
    private static final String iopts[] = {"inbound.length", "inbound.quantity",
                                           "outbound.length", "outbound.quantity" };

    /** put the individual i2cp selections into the option string */
    private static String buildI2CPOpts(HttpServletRequest req) {
        StringBuilder buf = new StringBuilder(128);
        String p = req.getParameter("i2cpOpts");
        if (p != null)
            buf.append(p);
        for (int i = 0; i < iopts.length; i++) {
            p = req.getParameter(iopts[i]);
            if (p != null)
                buf.append(' ').append(iopts[i]).append('=').append(p);
        }
        return buf.toString();
    }

    /**
     *  Sort alphabetically in current locale, ignore case, ignore leading "the "
     *  (I guess this is worth it, a lot of torrents start with "The "
     *  These are full path names which makes it harder
     *  @since 0.7.14
     */
    private class TorrentNameComparator implements Comparator<String> {
        private final Comparator collator = Collator.getInstance();
        private final String skip = _manager.getDataDir().getAbsolutePath() + File.separator;

        public int compare(String l, String r) {
            if (l.startsWith(skip))
                l = l.substring(skip.length());
            if (r.startsWith(skip))
                r = r.substring(skip.length());
            String llc = l.toLowerCase();
            if (llc.startsWith("the ") || llc.startsWith("the."))
                l = l.substring(4);
            String rlc = r.toLowerCase();
            if (rlc.startsWith("the ") || rlc.startsWith("the."))
                r = r.substring(4);
            return collator.compare(l, r);
        }
    }

    private List<Snark> getSortedSnarks(HttpServletRequest req) {
        Set<String> files = _manager.listTorrentFiles();
        TreeSet<String> fileNames = new TreeSet(new TorrentNameComparator());
        fileNames.addAll(files);
        ArrayList<Snark> rv = new ArrayList(fileNames.size());
        for (Iterator iter = fileNames.iterator(); iter.hasNext(); ) {
            String name = (String)iter.next();
            Snark snark = _manager.getTorrent(name);
            if (snark != null)
                rv.add(snark);
        }
        return rv;
    }

    private static final int MAX_DISPLAYED_FILENAME_LENGTH = 44;
    private static final int MAX_DISPLAYED_ERROR_LENGTH = 40;
    private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers, boolean showDebug) throws IOException {
        String filename = snark.torrent;
        File f = new File(filename);
        filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name
        int i = filename.lastIndexOf(".torrent");
        if (i > 0)
            filename = filename.substring(0, i);
        String fullFilename = filename;
        if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH) {
            fullFilename = new String(filename);
            filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "&hellip;";
        }
        long total = snark.meta.getTotalLength();
        // Early typecast, avoid possibly overflowing a temp integer
        long remaining = (long) snark.storage.needed() * (long) snark.meta.getPieceLength(0); 
        if (remaining > total)
            remaining = total;
        long downBps = 0;
        long upBps = 0;
        if (snark.coordinator != null) {
            downBps = snark.coordinator.getDownloadRate();
            upBps = snark.coordinator.getUploadRate();
        }
        long remainingSeconds;
        if (downBps > 0)
            remainingSeconds = remaining / downBps;
        else
            remainingSeconds = -1;
        boolean isRunning = !snark.stopped;
        long uploaded = 0;
        if (snark.coordinator != null) {
            uploaded = snark.coordinator.getUploaded();
            stats[0] += snark.coordinator.getDownloaded();
        }
        stats[1] += uploaded;
        if (isRunning) {
            stats[2] += downBps;
            stats[3] += upBps;
        }
        stats[5] += total;
        
        boolean isValid = snark.meta != null;
        boolean singleFile = snark.meta.getFiles() == null;
        
        String err = null;
        int curPeers = 0;
        int knownPeers = 0;
        if (snark.coordinator != null) {
            err = snark.coordinator.trackerProblems;
            curPeers = snark.coordinator.getPeerCount();
            stats[4] += curPeers;
            knownPeers = Math.max(curPeers, snark.coordinator.trackerSeenPeers);
        }
        
        String statusString = _("Unknown");
        if (err != null) {
            if (isRunning && curPeers > 0 && !showPeers)
                statusString = "<a title=\"" + err + "\">" + _("TrackerErr") + "</a> (" +
                               "<a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
                               curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + "</a>)";
            else if (isRunning)
                statusString = "<a title=\"" + err + "\">" + _("TrackerErr") + " (" + curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + ')';
            else {
                if (err.length() > MAX_DISPLAYED_ERROR_LENGTH)
                    err = err.substring(0, MAX_DISPLAYED_ERROR_LENGTH) + "&hellip;";
                statusString = _("TrackerErr") + "<br>(" + err + ")";
            }
        } else if (remaining <= 0) {
            if (isRunning && curPeers > 0 && !showPeers)
                statusString = _("Seeding") + " (" +
                               "<a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
                               curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + "</a>)";
            else if (isRunning)
                statusString = _("Seeding") + " (" + curPeers + "/" +
                               ngettext("1 peer", "{0} peers", knownPeers) + ')';
            else
                statusString = _("Complete");
        } else {
            if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
                statusString = _("OK") + " (" +
                               "<a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
                               curPeers + "/" +
                               ngettext("1 peer", "{0} peers", knownPeers) + "</a>)";
            else if (isRunning && curPeers > 0 && downBps > 0)
                statusString = _("OK") + " (" + curPeers + "/" +
                               ngettext("1 peer", "{0} peers", knownPeers) + ')';
            else if (isRunning && curPeers > 0 && !showPeers)
                statusString = _("Stalled") + " (" +
                               "<a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
                               curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + "</a>)";
            else if (isRunning && curPeers > 0)
                statusString = _("Stalled") + " (" + curPeers + '/' +
                               ngettext("1 peer", "{0} peers", knownPeers) + ')';
            else if (isRunning)
                statusString = _("No Peers") + " (0/" + knownPeers + ')';
            else
                statusString = _("Stopped");
        }
        
        String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
        out.write("<tr class=\"" + rowClass + "\">");
        out.write("<td align=\"center\" class=\"snarkTorrentStatus " + rowClass + "\">");
        out.write(statusString + "</td>\n\t");
        out.write("<td align=\"left\" class=\"snarkTorrentName " + rowClass + "\">");
        
        if (remaining == 0 || snark.meta.getFiles() != null) {
            out.write("<a href=\"" + snark.storage.getBaseName());
            if (snark.meta.getFiles() != null)
                out.write("/");
            out.write("\" title=\"");
            if (snark.meta.getFiles() != null)
                out.write(_("View files"));
            else
                out.write(_("Open file"));
            out.write("\">");
        }
        String icon;
        if (snark.meta.getFiles() != null)
            icon = "folder";
        else
            icon = toIcon(snark.meta.getName());
        out.write(toImg(icon));
        out.write(filename);
        if (remaining == 0 || snark.meta.getFiles() != null)
            out.write("</a>");
        // temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash
        String announce = snark.meta.getAnnounce();
        if (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") ||
            announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/")) {
            Map trackers = _manager.getTrackers();
            for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
                Map.Entry entry = (Map.Entry)iter.next();
                String name = (String)entry.getKey();
                String baseURL = (String)entry.getValue();
                if (!(baseURL.startsWith(announce) || // vvv hack for non-b64 announce in list vvv
                      (announce.startsWith("http://lnQ6yoBT") && baseURL.startsWith("http://tracker2.postman.i2p/"))))
                    continue;
                int e = baseURL.indexOf('=');
                if (e < 0)
                    continue;
                baseURL = baseURL.substring(e + 1);
                out.write("&nbsp;&nbsp;&nbsp;[<a href=\"" + baseURL + "details.php?dllist=1&filelist=1&info_hash=");
                out.write(TrackerClient.urlencode(snark.meta.getInfoHash()));
                out.write("\" title=\"" + name + ' ' + _("Tracker") + "\">");
                out.write(_("Details"));
                out.write("</a>]");
                break;
            }
        }
        out.write("</td>\n\t");
        
        out.write("<td align=\"center\" class=\"snarkTorrentETA " + rowClass + "\">");
        if(isRunning && remainingSeconds > 0)
            out.write(DataHelper.formatDuration(remainingSeconds*1000)); // (eta 6h)
        out.write("</td>\n\t");
        out.write("<td align=\"right\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
        if (remaining > 0)
            out.write(formatSize(total-remaining) + " / " + formatSize(total)); // 18MB/3GB
        else
            out.write(formatSize(total)); // 3GB
        out.write("</td>\n\t");
        out.write("<td align=\"right\" class=\"snarkTorrentUploaded " + rowClass 
                  + "\">" + formatSize(uploaded) + "</td>\n\t");
        out.write("<td align=\"right\" class=\"snarkTorrentRate\">");
        if(isRunning && remaining > 0)
            out.write(formatSize(downBps) + "ps");
        out.write("</td>\n\t");
        out.write("<td align=\"right\" class=\"snarkTorrentRate\">");
        if(isRunning)
            out.write(formatSize(upBps) + "ps");
        out.write("</td>\n\t");
        out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">");
        String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.meta.getInfoHash());
        if (showPeers)
            parameters = parameters + "&p=1";
        if (isRunning) {
            out.write("<a href=\"" + uri + "?action=Stop" + parameters
                      + "\" title=\"");
            out.write(_("Stop the torrent"));
            out.write("\">");
            out.write(_("Stop"));
            out.write("</a>");
        } else {
            if (isValid) {
                out.write("<a href=\"" + uri + "?action=Start" + parameters
                          + "\" title=\"");
                out.write(_("Start the torrent"));
                out.write("\">");
                out.write(_("Start"));
                out.write("</a>\n");
            }
            out.write("<a href=\"" + uri + "?action=Remove" + parameters
                      + "\" title=\"");
            out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
            out.write("\" onclick=\"if (!confirm('");
            // Can't figure out how to escape double quotes inside the onclick string.
            // Single quotes in translate strings with parameters must be doubled.
            // Then the remaining single quite must be escaped
            out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
            out.write("')) { return false; }\">");
            out.write(_("Remove"));
            out.write("</a><br>");
            out.write("<a href=\"" + uri + "?action=Delete" + parameters
                      + "\" title=\"");
            out.write(_("Delete the .torrent file and the associated data file(s)"));
            out.write("\" onclick=\"if (!confirm('");
            // Can't figure out how to escape double quotes inside the onclick string.
            // Single quotes in translate strings with parameters must be doubled.
            // Then the remaining single quite must be escaped
            out.write(_("Are you sure you want to delete the torrent \\''{0}\\'' and all downloaded data?", fullFilename));
            out.write("')) { return false; }\">");
            out.write(_("Delete"));
            out.write("</a>");
        }
        out.write("</td>\n</tr>\n");

        if(showPeers && isRunning && curPeers > 0) {
            List<Peer> peers = snark.coordinator.peerList();
            if (!showDebug)
                Collections.sort(peers, new PeerComparator());
            for (Peer peer : peers) {
                if (!peer.isConnected())
                    continue;
                out.write("<tr class=\"" + rowClass + "\">");
                out.write("<td align=\"center\" class=\"snarkTorrentStatus " + rowClass + "\">");
                out.write("</td>\n\t");
                out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
{'fraction_non_alphanumeric': 0.15161821039190648, 'fraction_numerical': 0.006679108142618603, 'mean_word_length': 1.6619930058502468, 'pattern_counts': {'":': 4, '<': 297, '<?xml version=': 0, '>': 325, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 62, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '827666', 'n_tokens_mistral': 22514, 'n_tokens_neox': 21752, 'n_words': 4968}