diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index e0ff254b8e85f1e98b7ca7eaf9145b3fce40316e..bf7c146cb9e74e94e4279661db07840d5088b86e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -1,760 +1,760 @@ -package org.klomp.snark; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.TreeMap; - -import net.i2p.I2PAppContext; -import net.i2p.data.Base64; -import net.i2p.data.DataHelper; -import net.i2p.util.I2PAppThread; -import net.i2p.util.Log; - -/** - * Manage multiple snarks - */ -public class SnarkManager implements Snark.CompleteListener { - private static SnarkManager _instance = new SnarkManager(); - public static SnarkManager instance() { return _instance; } - - /** map of (canonical) filename to Snark instance (unsynchronized) */ - private final Map _snarks; - private final Object _addSnarkLock; - private /* FIXME final FIXME */ File _configFile; - private Properties _config; - private I2PAppContext _context; - private Log _log; - private final List _messages; - private I2PSnarkUtil _util; - private PeerCoordinatorSet _peerCoordinatorSet; - private ConnectionAcceptor _connectionAcceptor; - - public static final String PROP_I2CP_HOST = "i2psnark.i2cpHost"; - public static final String PROP_I2CP_PORT = "i2psnark.i2cpPort"; - public static final String PROP_I2CP_OPTS = "i2psnark.i2cpOptions"; - public static final String PROP_EEP_HOST = "i2psnark.eepHost"; - public static final String PROP_EEP_PORT = "i2psnark.eepPort"; - public static final String PROP_UPLOADERS_TOTAL = "i2psnark.uploaders.total"; - public static final String PROP_UPBW_MAX = "i2psnark.upbw.max"; - public static final String PROP_DIR = "i2psnark.dir"; - public static final String PROP_META_PREFIX = "i2psnark.zmeta."; - public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield"; - - private static final String CONFIG_FILE = "i2psnark.config"; - public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops - public static final String DEFAULT_AUTO_START = "false"; - public static final String PROP_LINK_PREFIX = "i2psnark.linkPrefix"; - public static final String DEFAULT_LINK_PREFIX = "file:///"; - - public static final int MIN_UP_BW = 2; - public static final int DEFAULT_MAX_UP_BW = 10; - - private SnarkManager() { - _snarks = new HashMap(); - _addSnarkLock = new Object(); - _context = I2PAppContext.getGlobalContext(); - _log = _context.logManager().getLog(SnarkManager.class); - _messages = new ArrayList(16); - _util = new I2PSnarkUtil(_context); - _configFile = new File(CONFIG_FILE); - if (!_configFile.isAbsolute()) - _configFile = new File(_context.getConfigDir(), CONFIG_FILE); - loadConfig(null); - } - - /** Caller _must_ call loadConfig(file) before this if setting new values - * for i2cp host/port or i2psnark.dir - */ - public void start() { - _peerCoordinatorSet = new PeerCoordinatorSet(); - _connectionAcceptor = new ConnectionAcceptor(_util); - int minutes = getStartupDelayMinutes(); - _messages.add("Adding torrents in " + minutes + (minutes == 1 ? " minute" : " minutes")); - I2PAppThread monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor"); - monitor.setDaemon(true); - monitor.start(); - _context.addShutdownTask(new SnarkManagerShutdown()); - } - - /** hook to I2PSnarkUtil for the servlet */ - public I2PSnarkUtil util() { return _util; } - - private static final int MAX_MESSAGES = 5; - public void addMessage(String message) { - synchronized (_messages) { - _messages.add(message); - while (_messages.size() > MAX_MESSAGES) - _messages.remove(0); - } - if (_log.shouldLog(Log.INFO)) - _log.info("MSG: " + message); - } - - /** newest last */ - public List getMessages() { - synchronized (_messages) { - return new ArrayList(_messages); - } - } - - public boolean shouldAutoStart() { - return Boolean.valueOf(_config.getProperty(PROP_AUTO_START, DEFAULT_AUTO_START+"")).booleanValue(); - } - public String linkPrefix() { - return _config.getProperty(PROP_LINK_PREFIX, DEFAULT_LINK_PREFIX + getDataDir().getAbsolutePath() + File.separatorChar); - } - private int getStartupDelayMinutes() { return 3; } - public File getDataDir() { - String dir = _config.getProperty(PROP_DIR, "i2psnark"); - File f = new File(dir); - if (!f.isAbsolute()) - f = new File(_context.getAppDir(), dir); - return f; - } - - /** null to set initial defaults */ - public void loadConfig(String filename) { - if (_config == null) - _config = new Properties(); - if (filename != null) { - File cfg = new File(filename); - if (!cfg.isAbsolute()) - cfg = new File(_context.getConfigDir(), filename); - _configFile = cfg; - if (cfg.exists()) { - try { - DataHelper.loadProps(_config, cfg); - } catch (IOException ioe) { - _log.error("Error loading I2PSnark config '" + filename + "'", ioe); - } - } - } - // now add sane defaults - if (!_config.containsKey(PROP_I2CP_HOST)) - _config.setProperty(PROP_I2CP_HOST, "127.0.0.1"); - if (!_config.containsKey(PROP_I2CP_PORT)) - _config.setProperty(PROP_I2CP_PORT, "7654"); - if (!_config.containsKey(PROP_I2CP_OPTS)) - _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0 inbound.quantity=3 outbound.quantity=3"); - if (!_config.containsKey(PROP_EEP_HOST)) - _config.setProperty(PROP_EEP_HOST, "127.0.0.1"); - if (!_config.containsKey(PROP_EEP_PORT)) - _config.setProperty(PROP_EEP_PORT, "4444"); - if (!_config.containsKey(PROP_UPLOADERS_TOTAL)) - _config.setProperty(PROP_UPLOADERS_TOTAL, "" + Snark.MAX_TOTAL_UPLOADERS); - if (!_config.containsKey(PROP_DIR)) - _config.setProperty(PROP_DIR, "i2psnark"); - if (!_config.containsKey(PROP_AUTO_START)) - _config.setProperty(PROP_AUTO_START, DEFAULT_AUTO_START); - updateConfig(); - } - - /** call from DirMonitor since loadConfig() is called before router I2CP is up */ - private void getBWLimit() { - if (!_config.containsKey(PROP_UPBW_MAX)) { - int[] limits = BWLimits.getBWLimits(_util.getI2CPHost(), _util.getI2CPPort()); - if (limits != null && limits[1] > 0) - _util.setMaxUpBW(limits[1]); - } - } - - private void updateConfig() { - String i2cpHost = _config.getProperty(PROP_I2CP_HOST); - int i2cpPort = getInt(PROP_I2CP_PORT, 7654); - String opts = _config.getProperty(PROP_I2CP_OPTS); - Map i2cpOpts = new HashMap(); - if (opts != null) { - StringTokenizer tok = new StringTokenizer(opts, " "); - while (tok.hasMoreTokens()) { - String pair = tok.nextToken(); - int split = pair.indexOf('='); - if (split > 0) - i2cpOpts.put(pair.substring(0, split), pair.substring(split+1)); - } - } - if (i2cpHost != null) { - _util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts); - _log.debug("Configuring with I2CP options " + i2cpOpts); - } - //I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties()); - String eepHost = _config.getProperty(PROP_EEP_HOST); - int eepPort = getInt(PROP_EEP_PORT, 4444); - if (eepHost != null) - _util.setProxy(eepHost, eepPort); - _util.setMaxUploaders(getInt(PROP_UPLOADERS_TOTAL, Snark.MAX_TOTAL_UPLOADERS)); - _util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW)); - getDataDir().mkdirs(); - } - - private int getInt(String prop, int defaultVal) { - String p = _config.getProperty(prop); - try { - if ( (p != null) && (p.trim().length() > 0) ) - return Integer.parseInt(p.trim()); - } catch (NumberFormatException nfe) { - // ignore - } - return defaultVal; - } - - public void updateConfig(String dataDir, boolean autoStart, String seedPct, String eepHost, - String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts, - String upLimit, String upBW, boolean useOpenTrackers, String openTrackers) { - boolean changed = false; - if (eepHost != null) { - int port = _util.getEepProxyPort(); - try { port = Integer.parseInt(eepPort); } catch (NumberFormatException nfe) {} - String host = _util.getEepProxyHost(); - if ( (eepHost.trim().length() > 0) && (port > 0) && - ((!host.equals(eepHost) || (port != _util.getEepProxyPort()) )) ) { - _util.setProxy(eepHost, port); - changed = true; - _config.setProperty(PROP_EEP_HOST, eepHost); - _config.setProperty(PROP_EEP_PORT, eepPort+""); - addMessage("EepProxy location changed to " + eepHost + ":" + port); - } - } - if (upLimit != null) { - int limit = _util.getMaxUploaders(); - try { limit = Integer.parseInt(upLimit); } catch (NumberFormatException nfe) {} - if ( limit != _util.getMaxUploaders()) { - if ( limit >= Snark.MIN_TOTAL_UPLOADERS ) { - _util.setMaxUploaders(limit); - changed = true; - _config.setProperty(PROP_UPLOADERS_TOTAL, "" + limit); - addMessage("Total uploaders limit changed to " + limit); - } else { - addMessage("Minimum total uploaders limit is " + Snark.MIN_TOTAL_UPLOADERS); - } - } - } - if (upBW != null) { - int limit = _util.getMaxUpBW(); - try { limit = Integer.parseInt(upBW); } catch (NumberFormatException nfe) {} - if ( limit != _util.getMaxUpBW()) { - if ( limit >= MIN_UP_BW ) { - _util.setMaxUpBW(limit); - changed = true; - _config.setProperty(PROP_UPBW_MAX, "" + limit); - addMessage("Up BW limit changed to " + limit + "KBps"); - } else { - addMessage("Minimum Up BW limit is " + MIN_UP_BW + "KBps"); - } - } - } - if (i2cpHost != null) { - int oldI2CPPort = _util.getI2CPPort(); - String oldI2CPHost = _util.getI2CPHost(); - int port = oldI2CPPort; - try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {} - String host = oldI2CPHost; - Map opts = new HashMap(); - if (i2cpOpts == null) i2cpOpts = ""; - StringTokenizer tok = new StringTokenizer(i2cpOpts, " \t\n"); - while (tok.hasMoreTokens()) { - String pair = tok.nextToken(); - int split = pair.indexOf('='); - if (split > 0) - opts.put(pair.substring(0, split), pair.substring(split+1)); - } - Map oldOpts = new HashMap(); - String oldI2CPOpts = _config.getProperty(PROP_I2CP_OPTS); - if (oldI2CPOpts == null) oldI2CPOpts = ""; - tok = new StringTokenizer(oldI2CPOpts, " \t\n"); - while (tok.hasMoreTokens()) { - String pair = tok.nextToken(); - int split = pair.indexOf('='); - if (split > 0) - oldOpts.put(pair.substring(0, split), pair.substring(split+1)); - } - - if ( (i2cpHost.trim().length() > 0) && (port > 0) && - ((!host.equals(i2cpHost) || - (port != _util.getI2CPPort()) || - (!oldOpts.equals(opts)))) ) { - boolean snarksActive = false; - Set names = listTorrentFiles(); - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - Snark snark = getTorrent((String)iter.next()); - if ( (snark != null) && (!snark.stopped) ) { - snarksActive = true; - break; - } - } - if (snarksActive) { - addMessage("Cannot change the I2CP settings while torrents are active"); - _log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts - + "] oldOpts [" + oldOpts + "]"); - } else { - if (_util.connected()) { - _util.disconnect(); - addMessage("Disconnecting old I2CP destination"); - } - Properties p = new Properties(); - p.putAll(opts); - addMessage("I2CP settings changed to " + i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")"); - _util.setI2CPConfig(i2cpHost, port, p); - boolean ok = _util.connect(); - if (!ok) { - addMessage("Unable to connect with the new settings, reverting to the old I2CP settings"); - _util.setI2CPConfig(oldI2CPHost, oldI2CPPort, oldOpts); - ok = _util.connect(); - if (!ok) - addMessage("Unable to reconnect with the old settings!"); - } else { - addMessage("Reconnected on the new I2CP destination"); - _config.setProperty(PROP_I2CP_HOST, i2cpHost.trim()); - _config.setProperty(PROP_I2CP_PORT, "" + port); - _config.setProperty(PROP_I2CP_OPTS, i2cpOpts.trim()); - changed = true; - // no PeerAcceptors/I2PServerSockets to deal with, since all snarks are inactive - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - Snark snark = getTorrent(name); - if ( (snark != null) && (snark.acceptor != null) ) { - snark.acceptor.restart(); - addMessage("I2CP listener restarted for " + snark.meta.getName()) +"."; - } - } - } - } - changed = true; - } - } - if (shouldAutoStart() != autoStart) { - _config.setProperty(PROP_AUTO_START, autoStart + ""); - addMessage("Autostart of torrents set to " + autoStart); - changed = true + "."; - } - if (_util.shouldUseOpenTrackers() != useOpenTrackers) { - _config.setProperty(I2PSnarkUtil.PROP_USE_OPENTRACKERS, useOpenTrackers + ""); - addMessage((useOpenTrackers ? "En" : "Dis") + "abled open trackers - torrent restart required to take effect."); - changed = true; - } - if (openTrackers != null) { - if (openTrackers.trim().length() > 0 && !openTrackers.trim().equals(_util.getOpenTrackerString())) { - _config.setProperty(I2PSnarkUtil.PROP_OPENTRACKERS, openTrackers.trim()); - addMessage("Open Tracker list changed - torrent restart required to take effect."); - changed = true; - } - } - if (changed) { - saveConfig(); - } else { - addMessage("Configuration unchanged."); - } - } - - public void saveConfig() { - try { - synchronized (_configFile) { - DataHelper.storeProps(_config, _configFile); - } - } catch (IOException ioe) { - addMessage("Unable to save the config to '" + _configFile.getAbsolutePath() + "'."); - } - } - - public Properties getConfig() { return _config; } - - /** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */ - private static final int MAX_FILES_PER_TORRENT = 512; - - /** set of filenames that we are dealing with */ - public Set listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } } - /** - * Grab the torrent given the (canonical) filename - */ - public Snark getTorrent(String filename) { synchronized (_snarks) { return (Snark)_snarks.get(filename); } } - public void addTorrent(String filename) { addTorrent(filename, false); } - public void addTorrent(String filename, boolean dontAutoStart) { - if ((!dontAutoStart) && !_util.connected()) { - addMessage("Connecting to I2P"); - boolean ok = _util.connect(); - if (!ok) { - addMessage("Error connecting to I2P - check your I2CP settings!"); - return; - } - } - File sfile = new File(filename); - try { - filename = sfile.getCanonicalPath(); - } catch (IOException ioe) { - _log.error("Unable to add the torrent " + filename, ioe) + "."; - addMessage("ERR: Could not add the torrent '" + filename + "': " + ioe.getMessage()) + "."; - return; - } - File dataDir = getDataDir(); - Snark torrent = null; - synchronized (_snarks) { - torrent = (Snark)_snarks.get(filename); - } - // don't hold the _snarks lock while verifying the torrent - if (torrent == null) { - synchronized (_addSnarkLock) { - // double-check - synchronized (_snarks) { - if(_snarks.get(filename) != null) - return; - } - - FileInputStream fis = null; - try { - fis = new FileInputStream(sfile); - MetaInfo info = new MetaInfo(fis); - fis.close(); - fis = null; - - String rejectMessage = locked_validateTorrent(info); - if (rejectMessage != null) { - sfile.delete(); - addMessage(rejectMessage); - return; - } else { - torrent = new Snark(_util, filename, null, -1, null, null, this, - _peerCoordinatorSet, _connectionAcceptor, - false, dataDir.getPath()); - torrent.completeListener = this; - synchronized (_snarks) { - _snarks.put(filename, torrent); - } - } - } catch (IOException ioe) { - addMessage("Torrent in " + sfile.getName() + " is invalid: " + ioe.getMessage()) +"."; - if (sfile.exists()) - sfile.delete(); - return; - } finally { - if (fis != null) try { fis.close(); } catch (IOException ioe) {} - } - } - } else { - return; - } - // ok, snark created, now lets start it up or configure it further - File f = new File(filename); - if (!dontAutoStart && shouldAutoStart()) { - torrent.startTorrent(); - addMessage("Torrent added and started: '" + f.getName() + "'."); - } else { - addMessage("Torrent added: '" + f.getName() + "'."); - } - } - - /** - * Get the timestamp for a torrent from the config file - */ - public long getSavedTorrentTime(Snark snark) { - MetaInfo metainfo = snark.meta; - byte[] ih = metainfo.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); - if (time == null) - return 0; - int comma = time.indexOf(','); - if (comma <= 0) - return 0; - time = time.substring(0, comma); - try { return Long.parseLong(time); } catch (NumberFormatException nfe) {} - return 0; - } - - /** - * Get the saved bitfield for a torrent from the config file. - * Convert "." to a full bitfield. - */ - public BitField getSavedTorrentBitField(Snark snark) { - MetaInfo metainfo = snark.meta; - byte[] ih = metainfo.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); - if (bf == null) - return null; - int comma = bf.indexOf(','); - if (comma <= 0) - return null; - bf = bf.substring(comma + 1).trim(); - int len = metainfo.getPieces(); - if (bf.equals(".")) { - BitField bitfield = new BitField(len); - for (int i = 0; i < len; i++) - bitfield.set(i); - return bitfield; - } - byte[] bitfield = Base64.decode(bf); - if (bitfield == null) - return null; - if (bitfield.length * 8 < len) - return null; - return new BitField(bitfield, len); - } - - /** - * Save the completion status of a torrent and the current time in the config file - * in the form "i2psnark.zmeta.$base64infohash=$time,$base64bitfield". - * The config file property key is appended with the Base64 of the infohash, - * with the '=' changed to '$' since a key can't contain '='. - * The time is a standard long converted to string. - * The status is either a bitfield converted to Base64 or "." for a completed - * torrent to save space in the config file and in memory. - */ - public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield) { - byte[] ih = metainfo.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String now = "" + System.currentTimeMillis(); - String bfs; - if (bitfield.complete()) { - bfs = "."; - } else { - byte[] bf = bitfield.getFieldBytes(); - bfs = Base64.encode(bf); - } - _config.setProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX, now + "," + bfs); - saveConfig(); - } - - /** - * Remove the status of a torrent from the config file. - * This may help the config file from growing too big. - */ - public void removeTorrentStatus(MetaInfo metainfo) { - byte[] ih = metainfo.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - _config.remove(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); - saveConfig(); - } - - private String locked_validateTorrent(MetaInfo info) throws IOException { - String announce = info.getAnnounce(); - // basic validation of url - if ((!announce.startsWith("http://")) || - (announce.indexOf(".i2p/") < 0)) // need to do better than this - return "Non-i2p tracker in " + info.getName() + ", deleting it from our list of trackers!"; - List files = info.getFiles(); - if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) { - return "Too many files in " + info.getName() + " (" + files.size() + "), deleting it!"; - } else if (info.getPieces() <= 0) { - return "No pieces in " + info.getName() + "? deleting it!"; - } else if (info.getPieceLength(0) > Storage.MAX_PIECE_SIZE) { - return "Pieces are too large in " + info.getName() + " (" + DataHelper.formatSize(info.getPieceLength(0)) + - "B), deleting it."; - } else if (info.getTotalLength() > Storage.MAX_TOTAL_SIZE) { - System.out.println("torrent info: " + info.toString()); - List lengths = info.getLengths(); - if (lengths != null) - for (int i = 0; i < lengths.size(); i++) - System.out.println("File " + i + " is " + lengths.get(i) + " long."); - - return "Torrents larger than " + DataHelper.formatSize(Storage.MAX_TOTAL_SIZE) + - "B are not supported yet (because we're paranoid): " + info.getName() + ", deleting it!"; - } else { - // ok - return null; - } - } - - /** - * Stop the torrent, leaving it on the list of torrents unless told to remove it - */ - public Snark stopTorrent(String filename, boolean shouldRemove) { - File sfile = new File(filename); - try { - filename = sfile.getCanonicalPath(); - } catch (IOException ioe) { - _log.error("Unable to remove the torrent " + filename, ioe); - addMessage("ERR: Could not remove the torrent '" + filename + "': " + ioe.getMessage()); - return null; - } - int remaining = 0; - Snark torrent = null; - synchronized (_snarks) { - if (shouldRemove) - torrent = (Snark)_snarks.remove(filename); - else - torrent = (Snark)_snarks.get(filename); - remaining = _snarks.size(); - } - if (torrent != null) { - boolean wasStopped = torrent.stopped; - torrent.stopTorrent(); - if (remaining == 0) { - // should we disconnect/reconnect here (taking care to deal with the other thread's - // I2PServerSocket.accept() call properly?) - ////_util. - } - if (!wasStopped) - addMessage("Torrent stopped: '" + sfile.getName() + "'."); - } - return torrent; - } - /** - * Stop the torrent and delete the torrent file itself, but leaving the data - * behind. - */ - public void removeTorrent(String filename) { - Snark torrent = stopTorrent(filename, true); - if (torrent != null) { - File torrentFile = new File(filename); - torrentFile.delete(); - if (torrent.storage != null) - removeTorrentStatus(torrent.storage.getMetaInfo()); - addMessage("Torrent removed: '" + torrentFile.getName() + "'."); - } - } - - private class DirMonitor implements Runnable { - public void run() { - try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {} - // the first message was a "We are starting up in 1m" - synchronized (_messages) { - if (_messages.size() == 1) - _messages.remove(0); - } - - // here because we need to delay until I2CP is up - // although the user will see the default until then - getBWLimit(); - while (true) { - File dir = getDataDir(); - _log.debug("Directory Monitor loop over " + dir.getAbsolutePath()); - try { - monitorTorrents(dir); - } catch (Exception e) { - _log.error("Error in the DirectoryMonitor", e); - } - try { Thread.sleep(60*1000); } catch (InterruptedException ie) {} - } - } - } - - /** two listeners */ - public void torrentComplete(Snark snark) { - File f = new File(snark.torrent); - long len = snark.meta.getTotalLength(); - addMessage("Download finished: " + f.getName() + " (size: " + DataHelper.formatSize(len) + "B)"); - updateStatus(snark); - } - - public void updateStatus(Snark snark) { - saveTorrentStatus(snark.meta, snark.storage.getBitField()); - } - - private void monitorTorrents(File dir) { - String fileNames[] = dir.list(TorrentFilenameFilter.instance()); - List foundNames = new ArrayList(0); - if (fileNames != null) { - for (int i = 0; i < fileNames.length; i++) { - try { - foundNames.add(new File(dir, fileNames[i]).getCanonicalPath()); - } catch (IOException ioe) { - _log.error("Error resolving '" + fileNames[i] + "' in '" + dir, ioe); - } - } - } - - Set existingNames = listTorrentFiles(); - // lets find new ones first... - for (int i = 0; i < foundNames.size(); i++) { - if (existingNames.contains(foundNames.get(i))) { - // already known. noop - } else { - if (shouldAutoStart() && !_util.connect()) - addMessage("Unable to connect to I2P!"); - addTorrent((String)foundNames.get(i), !shouldAutoStart()); - } - } - // now lets see which ones have been removed... - for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - if (foundNames.contains(name)) { - // known and still there. noop - } else { - // known, but removed. drop it - stopTorrent(name, true); - } - } - } - - private static final String DEFAULT_TRACKERS[] = { -// "Postman", "http://YRgrgTLGnbTq2aZOZDJQ~o6Uk5k6TK-OZtx0St9pb0G-5EGYURZioxqYG8AQt~LgyyI~NCj6aYWpPO-150RcEvsfgXLR~CxkkZcVpgt6pns8SRc3Bi-QSAkXpJtloapRGcQfzTtwllokbdC-aMGpeDOjYLd8b5V9Im8wdCHYy7LRFxhEtGb~RL55DA8aYOgEXcTpr6RPPywbV~Qf3q5UK55el6Kex-6VCxreUnPEe4hmTAbqZNR7Fm0hpCiHKGoToRcygafpFqDw5frLXToYiqs9d4liyVB-BcOb0ihORbo0nS3CLmAwZGvdAP8BZ7cIYE3Z9IU9D1G8JCMxWarfKX1pix~6pIA-sp1gKlL1HhYhPMxwyxvuSqx34o3BqU7vdTYwWiLpGM~zU1~j9rHL7x60pVuYaXcFQDR4-QVy26b6Pt6BlAZoFmHhPcAuWfu-SFhjyZYsqzmEmHeYdAwa~HojSbofg0TMUgESRXMw6YThK1KXWeeJVeztGTz25sL8AAAA.i2p/announce.php=http://tracker.postman.i2p/" -// , "eBook", "http://E71FRom6PZNEqTN2Lr8P-sr23b7HJVC32KoGnVQjaX6zJiXwhJy2HsXob36Qmj81TYFZdewFZa9mSJ533UZgGyQkXo2ahctg82JKYZfDe5uDxAn1E9YPjxZCWJaFJh0S~UwSs~9AZ7UcauSJIoNtpxrtbmRNVFLqnkEDdLZi26TeucfOmiFmIWnVblLniWv3tG1boE9Abd-6j3FmYVrRucYuepAILYt6katmVNOk6sXmno1Eynrp~~MBuFq0Ko6~jsc2E2CRVYXDhGHEMdt-j6JUz5D7S2RIVzDRqQyAZLKJ7OdQDmI31przzmne1vOqqqLC~1xUumZVIvF~yOeJUGNjJ1Vx0J8i2BQIusn1pQJ6UCB~ZtZZLQtEb8EPVCfpeRi2ri1M5CyOuxN0V5ekmPHrYIBNevuTCRC26NP7ZS5VDgx1~NaC3A-CzJAE6f1QXi0wMI9aywNG5KGzOPifcsih8eyGyytvgLtrZtV7ykzYpPCS-rDfITncpn5hliPUAAAA.i2p/pub/bt/announce.php=http://de-ebook-archiv.i2p/pub/bt/" -// , "Gaytorrents", "http://uxPWHbK1OIj9HxquaXuhMiIvi21iK0~ZiG9d8G0840ZXIg0r6CbiV71xlsqmdnU6wm0T2LySriM0doW2gUigo-5BNkUquHwOjLROiETnB3ZR0Ml4IGa6QBPn1aAq2d9~g1r1nVjLE~pcFnXB~cNNS7kIhX1d6nLgYVZf0C2cZopEow2iWVUggGGnAA9mHjE86zLEnTvAyhbAMTqDQJhEuLa0ZYSORqzJDMkQt90MV4YMjX1ICY6RfUSFmxEqu0yWTrkHsTtRw48l~dz9wpIgc0a0T9C~eeWvmBFTqlJPtQZwntpNeH~jF7nlYzB58olgV2HHFYpVYD87DYNzTnmNWxCJ5AfDorm6AIUCV2qaE7tZtI1h6fbmGpGlPyW~Kw5GXrRfJwNvr6ajwAVi~bPVnrBwDZezHkfW4slOO8FACPR28EQvaTu9nwhAbqESxV2hCTq6vQSGjuxHeOuzBOEvRWkLKOHWTC09t2DbJ94FSqETmZopTB1ukEmaxRWbKSIaAAAA.i2p/announce.php=http://gaytorrents.i2p/" -// , "NickyB", "http://9On6d3cZ27JjwYCtyJJbowe054d5tFnfMjv4PHsYs-EQn4Y4mk2zRixatvuAyXz2MmRfXG-NAUfhKr0KCxRNZbvHmlckYfT-WBzwwpiMAl0wDFY~Pl8cqXuhfikSG5WrqdPfDNNIBuuznS0dqaczf~OyVaoEOpvuP3qV6wKqbSSLpjOwwAaQPHjlRtNIW8-EtUZp-I0LT45HSoowp~6b7zYmpIyoATvIP~sT0g0MTrczWhbVTUZnEkZeLhOR0Duw1-IRXI2KHPbA24wLO9LdpKKUXed05RTz0QklW5ROgR6TYv7aXFufX8kC0-DaKvQ5JKG~h8lcoHvm1RCzNqVE-2aiZnO2xH08H-iCWoLNJE-Td2kT-Tsc~3QdQcnEUcL5BF-VT~QYRld2--9r0gfGl-yDrJZrlrihHGr5J7ImahelNn9PpkVp6eIyABRmJHf2iicrk3CtjeG1j9OgTSwaNmEpUpn4aN7Kx0zNLdH7z6uTgCGD9Kmh1MFYrsoNlTp4AAAA.i2p/bittorrent/announce.php=http://nickyb.i2p/bittorrent/" -// , "Orion", "http://gKik1lMlRmuroXVGTZ~7v4Vez3L3ZSpddrGZBrxVriosCQf7iHu6CIk8t15BKsj~P0JJpxrofeuxtm7SCUAJEr0AIYSYw8XOmp35UfcRPQWyb1LsxUkMT4WqxAT3s1ClIICWlBu5An~q-Mm0VFlrYLIPBWlUFnfPR7jZ9uP5ZMSzTKSMYUWao3ejiykr~mtEmyls6g-ZbgKZawa9II4zjOy-hdxHgP-eXMDseFsrym4Gpxvy~3Fv9TuiSqhpgm~UeTo5YBfxn6~TahKtE~~sdCiSydqmKBhxAQ7uT9lda7xt96SS09OYMsIWxLeQUWhns-C~FjJPp1D~IuTrUpAFcVEGVL-BRMmdWbfOJEcWPZ~CBCQSO~VkuN1ebvIOr9JBerFMZSxZtFl8JwcrjCIBxeKPBmfh~xYh16BJm1BBBmN1fp2DKmZ2jBNkAmnUbjQOqWvUcehrykWk5lZbE7bjJMDFH48v3SXwRuDBiHZmSbsTY6zhGY~GkMQHNGxPMMSIAAAA.i2p/bt/announce.php=http://orion.i2p/bt/" -// , "anonymity", "http://8EoJZIKrWgGuDrxA3nRJs1jsPfiGwmFWL91hBrf0HA7oKhEvAna4Ocx47VLUR9retVEYBAyWFK-eZTPcvhnz9XffBEiJQQ~kFSCqb1fV6IfPiV3HySqi9U5Caf6~hC46fRd~vYnxmaBLICT3N160cxBETqH3v2rdxdJpvYt8q4nMk9LUeVXq7zqCTFLLG5ig1uKgNzBGe58iNcsvTEYlnbYcE930ABmrzj8G1qQSgSwJ6wx3tUQNl1z~4wSOUMan~raZQD60lRK70GISjoX0-D0Po9WmPveN3ES3g72TIET3zc3WPdK2~lgmKGIs8GgNLES1cXTolvbPhdZK1gxddRMbJl6Y6IPFyQ9o4-6Rt3Lp-RMRWZ2TG7j2OMcNSiOmATUhKEFBDfv-~SODDyopGBmfeLw16F4NnYednvn4qP10dyMHcUASU6Zag4mfc2-WivrOqeWhD16fVAh8MoDpIIT~0r9XmwdaVFyLcjbXObabJczxCAW3fodQUnvuSkwzAAAA.i2p/anonymityTracker/announce.php=http://anonymityweb.i2p/anonymityTracker/" -// , "The freak's tracker", "http://mHKva9x24E5Ygfey2llR1KyQHv5f8hhMpDMwJDg1U-hABpJ2NrQJd6azirdfaR0OKt4jDlmP2o4Qx0H598~AteyD~RJU~xcWYdcOE0dmJ2e9Y8-HY51ie0B1yD9FtIV72ZI-V3TzFDcs6nkdX9b81DwrAwwFzx0EfNvK1GLVWl59Ow85muoRTBA1q8SsZImxdyZ-TApTVlMYIQbdI4iQRwU9OmmtefrCe~ZOf4UBS9-KvNIqUL0XeBSqm0OU1jq-D10Ykg6KfqvuPnBYT1BYHFDQJXW5DdPKwcaQE4MtAdSGmj1epDoaEBUa9btQlFsM2l9Cyn1hzxqNWXELmx8dRlomQLlV4b586dRzW~fLlOPIGC13ntPXogvYvHVyEyptXkv890jC7DZNHyxZd5cyrKC36r9huKvhQAmNABT2Y~pOGwVrb~RpPwT0tBuPZ3lHYhBFYmD8y~AOhhNHKMLzea1rfwTvovBMByDdFps54gMN1mX4MbCGT4w70vIopS9yAAAA.i2p/bytemonsoon/announce.php" -// , "mastertracker", "http://VzXD~stRKbL3MOmeTn1iaCQ0CFyTmuFHiKYyo0Rd~dFPZFCYH-22rT8JD7i-C2xzYFa4jT5U2aqHzHI-Jre4HL3Ri5hFtZrLk2ax3ji7Qfb6qPnuYkuiF2E2UDmKUOppI8d9Ye7tjdhQVCy0izn55tBaB-U7UWdcvSK2i85sauyw3G0Gfads1Rvy5-CAe2paqyYATcDmGjpUNLoxbfv9KH1KmwRTNH6k1v4PyWYYnhbT39WfKMbBjSxVQRdi19cyJrULSWhjxaQfJHeWx5Z8Ev4bSPByBeQBFl2~4vqy0S5RypINsRSa3MZdbiAAyn5tr5slWR6QdoqY3qBQgBJFZppy-3iWkFqqKgSxCPundF8gdDLC5ddizl~KYcYKl42y9SGFHIukH-TZs8~em0~iahzsqWVRks3zRG~tlBcX2U3M2~OJs~C33-NKhyfZT7-XFBREvb8Szmd~p66jDxrwOnKaku-G6DyoQipJqIz4VHmY9-y5T8RrUcJcM-5lVoMpAAAA.i2p/announce.php=http://tracker.mastertracker.i2p/" -// , "Galen", "http://5jpwQMI5FT303YwKa5Rd38PYSX04pbIKgTaKQsWbqoWjIfoancFdWCShXHLI5G5ofOb0Xu11vl2VEMyPsg1jUFYSVnu4-VfMe3y4TKTR6DTpetWrnmEK6m2UXh91J5DZJAKlgmO7UdsFlBkQfR2rY853-DfbJtQIFl91tbsmjcA5CGQi4VxMFyIkBzv-pCsuLQiZqOwWasTlnzey8GcDAPG1LDcvfflGV~6F5no9mnuisZPteZKlrv~~TDoXTj74QjByWc4EOYlwqK8sbU9aOvz~s31XzErbPTfwiawiaZ0RUI-IDrKgyvmj0neuFTWgjRGVTH8bz7cBZIc3viy6ioD-eMQOrXaQL0TCWZUelRwHRvgdPiQrxdYQs7ixkajeHzxi-Pq0EMm5Vbh3j3Q9kfUFW3JjFDA-MLB4g6XnjCbM5J1rC0oOBDCIEfhQkszru5cyLjHiZ5yeA0VThgu~c7xKHybv~OMXION7V8pBKOgET7ZgAkw1xgYe3Kkyq5syAAAA.i2p/tr/announce.php=http://galen.i2p/tr/" - "POSTMAN", "http://tracker2.postman.i2p/announce.php=http://tracker2.postman.i2p/" - ,"WELTERDE", "http://BGKmlDOoH3RzFbPRfRpZV2FjpVj8~3moFftw5-dZfDf2070TOe8Tf2~DAVeaM6ZRLdmFEt~9wyFL8YMLMoLoiwGEH6IGW6rc45tstN68KsBDWZqkTohV1q9XFgK9JnCwE~Oi89xLBHsLMTHOabowWM6dkC8nI6QqJC2JODqLPIRfOVrDdkjLwtCrsckzLybNdFmgfoqF05UITDyczPsFVaHtpF1sRggOVmdvCM66otyonlzNcJbn59PA-R808vUrCPMGU~O9Wys0i-NoqtIbtWfOKnjCRFMNw5ex4n9m5Sxm9e20UkpKG6qzEuvKZWi8vTLe1NW~CBrj~vG7I3Ok4wybUFflBFOaBabxYJLlx4xTE1zJIVxlsekmAjckB4v-cQwulFeikR4LxPQ6mCQknW2HZ4JQIq6hL9AMabxjOlYnzh7kjOfRGkck8YgeozcyTvcDUcUsOuSTk06L4kdrv8h2Cozjbloi5zl6KTbj5ZTciKCxi73Pn9grICn-HQqEAAAA.i2p/a=http://tracker.welterde.i2p/stats?mode=top5" - , "CRSTRACK", "http://b4G9sCdtfvccMAXh~SaZrPqVQNyGQbhbYMbw6supq2XGzbjU4NcOmjFI0vxQ8w1L05twmkOvg5QERcX6Mi8NQrWnR0stLExu2LucUXg1aYjnggxIR8TIOGygZVIMV3STKH4UQXD--wz0BUrqaLxPhrm2Eh9Hwc8TdB6Na4ShQUq5Xm8D4elzNUVdpM~RtChEyJWuQvoGAHY3ppX-EJJLkiSr1t77neS4Lc-KofMVmgI9a2tSSpNAagBiNI6Ak9L1T0F9uxeDfEG9bBSQPNMOSUbAoEcNxtt7xOW~cNOAyMyGydwPMnrQ5kIYPY8Pd3XudEko970vE0D6gO19yoBMJpKx6Dh50DGgybLQ9CpRaynh2zPULTHxm8rneOGRcQo8D3mE7FQ92m54~SvfjXjD2TwAVGI~ae~n9HDxt8uxOecAAvjjJ3TD4XM63Q9TmB38RmGNzNLDBQMEmJFpqQU8YeuhnS54IVdUoVQFqui5SfDeLXlSkh4vYoMU66pvBfWbAAAA.i2p/tracker/announce.php=http://crstrack.i2p/tracker/" - - }; - - /** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */ - public static final String PROP_TRACKERS = "i2psnark.trackers"; - private static Map trackerMap = null; - /** sorted map of name to announceURL=baseURL */ - public Map getTrackers() { - if (trackerMap != null) // only do this once, can't be updated while running - return trackerMap; - Map rv = new TreeMap(); - String trackers = _config.getProperty(PROP_TRACKERS); - if ( (trackers == null) || (trackers.trim().length() <= 0) ) - trackers = _context.getProperty(PROP_TRACKERS); - if ( (trackers == null) || (trackers.trim().length() <= 0) ) { - for (int i = 0; i < DEFAULT_TRACKERS.length; i += 2) - rv.put(DEFAULT_TRACKERS[i], DEFAULT_TRACKERS[i+1]); - } else { - StringTokenizer tok = new StringTokenizer(trackers, ","); - while (tok.hasMoreTokens()) { - String pair = tok.nextToken(); - int split = pair.indexOf('='); - if (split <= 0) - continue; - String name = pair.substring(0, split).trim(); - String url = pair.substring(split+1).trim(); - if ( (name.length() > 0) && (url.length() > 0) ) - rv.put(name, url); - } - } - - trackerMap = rv; - return trackerMap; - } - - private static class TorrentFilenameFilter implements FilenameFilter { - private static final TorrentFilenameFilter _filter = new TorrentFilenameFilter(); - public static TorrentFilenameFilter instance() { return _filter; } - public boolean accept(File dir, String name) { - return (name != null) && (name.endsWith(".torrent")); - } - } - - public class SnarkManagerShutdown extends I2PAppThread { - @Override - public void run() { - Set names = listTorrentFiles(); - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - Snark snark = getTorrent((String)iter.next()); - if ( (snark != null) && (!snark.stopped) ) - snark.stopTorrent(); - } - } - } -} +package org.klomp.snark; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TreeMap; + +import net.i2p.I2PAppContext; +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.util.I2PAppThread; +import net.i2p.util.Log; + +/** + * Manage multiple snarks + */ +public class SnarkManager implements Snark.CompleteListener { + private static SnarkManager _instance = new SnarkManager(); + public static SnarkManager instance() { return _instance; } + + /** map of (canonical) filename to Snark instance (unsynchronized) */ + private final Map _snarks; + private final Object _addSnarkLock; + private /* FIXME final FIXME */ File _configFile; + private Properties _config; + private I2PAppContext _context; + private Log _log; + private final List _messages; + private I2PSnarkUtil _util; + private PeerCoordinatorSet _peerCoordinatorSet; + private ConnectionAcceptor _connectionAcceptor; + + public static final String PROP_I2CP_HOST = "i2psnark.i2cpHost"; + public static final String PROP_I2CP_PORT = "i2psnark.i2cpPort"; + public static final String PROP_I2CP_OPTS = "i2psnark.i2cpOptions"; + public static final String PROP_EEP_HOST = "i2psnark.eepHost"; + public static final String PROP_EEP_PORT = "i2psnark.eepPort"; + public static final String PROP_UPLOADERS_TOTAL = "i2psnark.uploaders.total"; + public static final String PROP_UPBW_MAX = "i2psnark.upbw.max"; + public static final String PROP_DIR = "i2psnark.dir"; + public static final String PROP_META_PREFIX = "i2psnark.zmeta."; + public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield"; + + private static final String CONFIG_FILE = "i2psnark.config"; + public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops + public static final String DEFAULT_AUTO_START = "false"; + public static final String PROP_LINK_PREFIX = "i2psnark.linkPrefix"; + public static final String DEFAULT_LINK_PREFIX = "file:///"; + + public static final int MIN_UP_BW = 2; + public static final int DEFAULT_MAX_UP_BW = 10; + + private SnarkManager() { + _snarks = new HashMap(); + _addSnarkLock = new Object(); + _context = I2PAppContext.getGlobalContext(); + _log = _context.logManager().getLog(SnarkManager.class); + _messages = new ArrayList(16); + _util = new I2PSnarkUtil(_context); + _configFile = new File(CONFIG_FILE); + if (!_configFile.isAbsolute()) + _configFile = new File(_context.getConfigDir(), CONFIG_FILE); + loadConfig(null); + } + + /** Caller _must_ call loadConfig(file) before this if setting new values + * for i2cp host/port or i2psnark.dir + */ + public void start() { + _peerCoordinatorSet = new PeerCoordinatorSet(); + _connectionAcceptor = new ConnectionAcceptor(_util); + int minutes = getStartupDelayMinutes(); + _messages.add("Adding torrents in " + minutes + (minutes == 1 ? " minute" : " minutes")); + I2PAppThread monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor"); + monitor.setDaemon(true); + monitor.start(); + _context.addShutdownTask(new SnarkManagerShutdown()); + } + + /** hook to I2PSnarkUtil for the servlet */ + public I2PSnarkUtil util() { return _util; } + + private static final int MAX_MESSAGES = 5; + public void addMessage(String message) { + synchronized (_messages) { + _messages.add(message); + while (_messages.size() > MAX_MESSAGES) + _messages.remove(0); + } + if (_log.shouldLog(Log.INFO)) + _log.info("MSG: " + message); + } + + /** newest last */ + public List getMessages() { + synchronized (_messages) { + return new ArrayList(_messages); + } + } + + public boolean shouldAutoStart() { + return Boolean.valueOf(_config.getProperty(PROP_AUTO_START, DEFAULT_AUTO_START+"")).booleanValue(); + } + public String linkPrefix() { + return _config.getProperty(PROP_LINK_PREFIX, DEFAULT_LINK_PREFIX + getDataDir().getAbsolutePath() + File.separatorChar); + } + private int getStartupDelayMinutes() { return 3; } + public File getDataDir() { + String dir = _config.getProperty(PROP_DIR, "i2psnark"); + File f = new File(dir); + if (!f.isAbsolute()) + f = new File(_context.getAppDir(), dir); + return f; + } + + /** null to set initial defaults */ + public void loadConfig(String filename) { + if (_config == null) + _config = new Properties(); + if (filename != null) { + File cfg = new File(filename); + if (!cfg.isAbsolute()) + cfg = new File(_context.getConfigDir(), filename); + _configFile = cfg; + if (cfg.exists()) { + try { + DataHelper.loadProps(_config, cfg); + } catch (IOException ioe) { + _log.error("Error loading I2PSnark config '" + filename + "'", ioe); + } + } + } + // now add sane defaults + if (!_config.containsKey(PROP_I2CP_HOST)) + _config.setProperty(PROP_I2CP_HOST, "127.0.0.1"); + if (!_config.containsKey(PROP_I2CP_PORT)) + _config.setProperty(PROP_I2CP_PORT, "7654"); + if (!_config.containsKey(PROP_I2CP_OPTS)) + _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0 inbound.quantity=3 outbound.quantity=3"); + if (!_config.containsKey(PROP_EEP_HOST)) + _config.setProperty(PROP_EEP_HOST, "127.0.0.1"); + if (!_config.containsKey(PROP_EEP_PORT)) + _config.setProperty(PROP_EEP_PORT, "4444"); + if (!_config.containsKey(PROP_UPLOADERS_TOTAL)) + _config.setProperty(PROP_UPLOADERS_TOTAL, "" + Snark.MAX_TOTAL_UPLOADERS); + if (!_config.containsKey(PROP_DIR)) + _config.setProperty(PROP_DIR, "i2psnark"); + if (!_config.containsKey(PROP_AUTO_START)) + _config.setProperty(PROP_AUTO_START, DEFAULT_AUTO_START); + updateConfig(); + } + + /** call from DirMonitor since loadConfig() is called before router I2CP is up */ + private void getBWLimit() { + if (!_config.containsKey(PROP_UPBW_MAX)) { + int[] limits = BWLimits.getBWLimits(_util.getI2CPHost(), _util.getI2CPPort()); + if (limits != null && limits[1] > 0) + _util.setMaxUpBW(limits[1]); + } + } + + private void updateConfig() { + String i2cpHost = _config.getProperty(PROP_I2CP_HOST); + int i2cpPort = getInt(PROP_I2CP_PORT, 7654); + String opts = _config.getProperty(PROP_I2CP_OPTS); + Map i2cpOpts = new HashMap(); + if (opts != null) { + StringTokenizer tok = new StringTokenizer(opts, " "); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split > 0) + i2cpOpts.put(pair.substring(0, split), pair.substring(split+1)); + } + } + if (i2cpHost != null) { + _util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts); + _log.debug("Configuring with I2CP options " + i2cpOpts); + } + //I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties()); + String eepHost = _config.getProperty(PROP_EEP_HOST); + int eepPort = getInt(PROP_EEP_PORT, 4444); + if (eepHost != null) + _util.setProxy(eepHost, eepPort); + _util.setMaxUploaders(getInt(PROP_UPLOADERS_TOTAL, Snark.MAX_TOTAL_UPLOADERS)); + _util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW)); + getDataDir().mkdirs(); + } + + private int getInt(String prop, int defaultVal) { + String p = _config.getProperty(prop); + try { + if ( (p != null) && (p.trim().length() > 0) ) + return Integer.parseInt(p.trim()); + } catch (NumberFormatException nfe) { + // ignore + } + return defaultVal; + } + + public void updateConfig(String dataDir, boolean autoStart, String seedPct, String eepHost, + String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts, + String upLimit, String upBW, boolean useOpenTrackers, String openTrackers) { + boolean changed = false; + if (eepHost != null) { + int port = _util.getEepProxyPort(); + try { port = Integer.parseInt(eepPort); } catch (NumberFormatException nfe) {} + String host = _util.getEepProxyHost(); + if ( (eepHost.trim().length() > 0) && (port > 0) && + ((!host.equals(eepHost) || (port != _util.getEepProxyPort()) )) ) { + _util.setProxy(eepHost, port); + changed = true; + _config.setProperty(PROP_EEP_HOST, eepHost); + _config.setProperty(PROP_EEP_PORT, eepPort+""); + addMessage("EepProxy location changed to " + eepHost + ":" + port); + } + } + if (upLimit != null) { + int limit = _util.getMaxUploaders(); + try { limit = Integer.parseInt(upLimit); } catch (NumberFormatException nfe) {} + if ( limit != _util.getMaxUploaders()) { + if ( limit >= Snark.MIN_TOTAL_UPLOADERS ) { + _util.setMaxUploaders(limit); + changed = true; + _config.setProperty(PROP_UPLOADERS_TOTAL, "" + limit); + addMessage("Total uploaders limit changed to " + limit); + } else { + addMessage("Minimum total uploaders limit is " + Snark.MIN_TOTAL_UPLOADERS); + } + } + } + if (upBW != null) { + int limit = _util.getMaxUpBW(); + try { limit = Integer.parseInt(upBW); } catch (NumberFormatException nfe) {} + if ( limit != _util.getMaxUpBW()) { + if ( limit >= MIN_UP_BW ) { + _util.setMaxUpBW(limit); + changed = true; + _config.setProperty(PROP_UPBW_MAX, "" + limit); + addMessage("Up BW limit changed to " + limit + "KBps"); + } else { + addMessage("Minimum Up BW limit is " + MIN_UP_BW + "KBps"); + } + } + } + if (i2cpHost != null) { + int oldI2CPPort = _util.getI2CPPort(); + String oldI2CPHost = _util.getI2CPHost(); + int port = oldI2CPPort; + try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {} + String host = oldI2CPHost; + Map opts = new HashMap(); + if (i2cpOpts == null) i2cpOpts = ""; + StringTokenizer tok = new StringTokenizer(i2cpOpts, " \t\n"); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split > 0) + opts.put(pair.substring(0, split), pair.substring(split+1)); + } + Map oldOpts = new HashMap(); + String oldI2CPOpts = _config.getProperty(PROP_I2CP_OPTS); + if (oldI2CPOpts == null) oldI2CPOpts = ""; + tok = new StringTokenizer(oldI2CPOpts, " \t\n"); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split > 0) + oldOpts.put(pair.substring(0, split), pair.substring(split+1)); + } + + if ( (i2cpHost.trim().length() > 0) && (port > 0) && + ((!host.equals(i2cpHost) || + (port != _util.getI2CPPort()) || + (!oldOpts.equals(opts)))) ) { + boolean snarksActive = false; + Set names = listTorrentFiles(); + for (Iterator iter = names.iterator(); iter.hasNext(); ) { + Snark snark = getTorrent((String)iter.next()); + if ( (snark != null) && (!snark.stopped) ) { + snarksActive = true; + break; + } + } + if (snarksActive) { + addMessage("Cannot change the I2CP settings while torrents are active"); + _log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts + + "] oldOpts [" + oldOpts + "]"); + } else { + if (_util.connected()) { + _util.disconnect(); + addMessage("Disconnecting old I2CP destination"); + } + Properties p = new Properties(); + p.putAll(opts); + addMessage("I2CP settings changed to " + i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")"); + _util.setI2CPConfig(i2cpHost, port, p); + boolean ok = _util.connect(); + if (!ok) { + addMessage("Unable to connect with the new settings, reverting to the old I2CP settings"); + _util.setI2CPConfig(oldI2CPHost, oldI2CPPort, oldOpts); + ok = _util.connect(); + if (!ok) + addMessage("Unable to reconnect with the old settings!"); + } else { + addMessage("Reconnected on the new I2CP destination"); + _config.setProperty(PROP_I2CP_HOST, i2cpHost.trim()); + _config.setProperty(PROP_I2CP_PORT, "" + port); + _config.setProperty(PROP_I2CP_OPTS, i2cpOpts.trim()); + changed = true; + // no PeerAcceptors/I2PServerSockets to deal with, since all snarks are inactive + for (Iterator iter = names.iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + Snark snark = getTorrent(name); + if ( (snark != null) && (snark.acceptor != null) ) { + snark.acceptor.restart(); + addMessage("I2CP listener restarted for " + snark.meta.getName()); + } + } + } + } + changed = true; + } + } + if (shouldAutoStart() != autoStart) { + _config.setProperty(PROP_AUTO_START, autoStart + ""); + addMessage("Adjusted autostart to " + autoStart); + changed = true; + } + if (_util.shouldUseOpenTrackers() != useOpenTrackers) { + _config.setProperty(I2PSnarkUtil.PROP_USE_OPENTRACKERS, useOpenTrackers + ""); + addMessage((useOpenTrackers ? "En" : "Dis") + "abled open trackers - torrent restart required to take effect."); + changed = true; + } + if (openTrackers != null) { + if (openTrackers.trim().length() > 0 && !openTrackers.trim().equals(_util.getOpenTrackerString())) { + _config.setProperty(I2PSnarkUtil.PROP_OPENTRACKERS, openTrackers.trim()); + addMessage("Open Tracker list changed - torrent restart required to take effect."); + changed = true; + } + } + if (changed) { + saveConfig(); + } else { + addMessage("Configuration unchanged."); + } + } + + public void saveConfig() { + try { + synchronized (_configFile) { + DataHelper.storeProps(_config, _configFile); + } + } catch (IOException ioe) { + addMessage("Unable to save the config to '" + _configFile.getAbsolutePath() + "'."); + } + } + + public Properties getConfig() { return _config; } + + /** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */ + private static final int MAX_FILES_PER_TORRENT = 512; + + /** set of filenames that we are dealing with */ + public Set listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } } + /** + * Grab the torrent given the (canonical) filename + */ + public Snark getTorrent(String filename) { synchronized (_snarks) { return (Snark)_snarks.get(filename); } } + public void addTorrent(String filename) { addTorrent(filename, false); } + public void addTorrent(String filename, boolean dontAutoStart) { + if ((!dontAutoStart) && !_util.connected()) { + addMessage("Connecting to I2P"); + boolean ok = _util.connect(); + if (!ok) { + addMessage("Error connecting to I2P - check your I2CP settings!"); + return; + } + } + File sfile = new File(filename); + try { + filename = sfile.getCanonicalPath(); + } catch (IOException ioe) { + _log.error("Unable to add the torrent " + filename, ioe); + addMessage("ERR: Could not add the torrent '" + filename + "': " + ioe.getMessage()); + return; + } + File dataDir = getDataDir(); + Snark torrent = null; + synchronized (_snarks) { + torrent = (Snark)_snarks.get(filename); + } + // don't hold the _snarks lock while verifying the torrent + if (torrent == null) { + synchronized (_addSnarkLock) { + // double-check + synchronized (_snarks) { + if(_snarks.get(filename) != null) + return; + } + + FileInputStream fis = null; + try { + fis = new FileInputStream(sfile); + MetaInfo info = new MetaInfo(fis); + fis.close(); + fis = null; + + String rejectMessage = locked_validateTorrent(info); + if (rejectMessage != null) { + sfile.delete(); + addMessage(rejectMessage); + return; + } else { + torrent = new Snark(_util, filename, null, -1, null, null, this, + _peerCoordinatorSet, _connectionAcceptor, + false, dataDir.getPath()); + torrent.completeListener = this; + synchronized (_snarks) { + _snarks.put(filename, torrent); + } + } + } catch (IOException ioe) { + addMessage("Torrent in " + sfile.getName() + " is invalid: " + ioe.getMessage()); + if (sfile.exists()) + sfile.delete(); + return; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + } + } else { + return; + } + // ok, snark created, now lets start it up or configure it further + File f = new File(filename); + if (!dontAutoStart && shouldAutoStart()) { + torrent.startTorrent(); + addMessage("Torrent added and started: '" + f.getName() + "'."); + } else { + addMessage("Torrent added: '" + f.getName() + "'."); + } + } + + /** + * Get the timestamp for a torrent from the config file + */ + public long getSavedTorrentTime(Snark snark) { + MetaInfo metainfo = snark.meta; + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + if (time == null) + return 0; + int comma = time.indexOf(','); + if (comma <= 0) + return 0; + time = time.substring(0, comma); + try { return Long.parseLong(time); } catch (NumberFormatException nfe) {} + return 0; + } + + /** + * Get the saved bitfield for a torrent from the config file. + * Convert "." to a full bitfield. + */ + public BitField getSavedTorrentBitField(Snark snark) { + MetaInfo metainfo = snark.meta; + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + if (bf == null) + return null; + int comma = bf.indexOf(','); + if (comma <= 0) + return null; + bf = bf.substring(comma + 1).trim(); + int len = metainfo.getPieces(); + if (bf.equals(".")) { + BitField bitfield = new BitField(len); + for (int i = 0; i < len; i++) + bitfield.set(i); + return bitfield; + } + byte[] bitfield = Base64.decode(bf); + if (bitfield == null) + return null; + if (bitfield.length * 8 < len) + return null; + return new BitField(bitfield, len); + } + + /** + * Save the completion status of a torrent and the current time in the config file + * in the form "i2psnark.zmeta.$base64infohash=$time,$base64bitfield". + * The config file property key is appended with the Base64 of the infohash, + * with the '=' changed to '$' since a key can't contain '='. + * The time is a standard long converted to string. + * The status is either a bitfield converted to Base64 or "." for a completed + * torrent to save space in the config file and in memory. + */ + public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield) { + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + String now = "" + System.currentTimeMillis(); + String bfs; + if (bitfield.complete()) { + bfs = "."; + } else { + byte[] bf = bitfield.getFieldBytes(); + bfs = Base64.encode(bf); + } + _config.setProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX, now + "," + bfs); + saveConfig(); + } + + /** + * Remove the status of a torrent from the config file. + * This may help the config file from growing too big. + */ + public void removeTorrentStatus(MetaInfo metainfo) { + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + _config.remove(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + saveConfig(); + } + + private String locked_validateTorrent(MetaInfo info) throws IOException { + String announce = info.getAnnounce(); + // basic validation of url + if ((!announce.startsWith("http://")) || + (announce.indexOf(".i2p/") < 0)) // need to do better than this + return "Non-i2p tracker in " + info.getName() + ", deleting it from our list of trackers!"; + List files = info.getFiles(); + if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) { + return "Too many files in " + info.getName() + " (" + files.size() + "), deleting it!"; + } else if (info.getPieces() <= 0) { + return "No pieces in " + info.getName() + "? deleting it!"; + } else if (info.getPieceLength(0) > Storage.MAX_PIECE_SIZE) { + return "Pieces are too large in " + info.getName() + " (" + DataHelper.formatSize(info.getPieceLength(0)) + + "B), deleting it."; + } else if (info.getTotalLength() > Storage.MAX_TOTAL_SIZE) { + System.out.println("torrent info: " + info.toString()); + List lengths = info.getLengths(); + if (lengths != null) + for (int i = 0; i < lengths.size(); i++) + System.out.println("File " + i + " is " + lengths.get(i) + " long."); + + return "Torrents larger than " + DataHelper.formatSize(Storage.MAX_TOTAL_SIZE) + + "B are not supported yet (because we're paranoid): " + info.getName() + ", deleting it!"; + } else { + // ok + return null; + } + } + + /** + * Stop the torrent, leaving it on the list of torrents unless told to remove it + */ + public Snark stopTorrent(String filename, boolean shouldRemove) { + File sfile = new File(filename); + try { + filename = sfile.getCanonicalPath(); + } catch (IOException ioe) { + _log.error("Unable to remove the torrent " + filename, ioe); + addMessage("ERR: Could not remove the torrent '" + filename + "': " + ioe.getMessage()); + return null; + } + int remaining = 0; + Snark torrent = null; + synchronized (_snarks) { + if (shouldRemove) + torrent = (Snark)_snarks.remove(filename); + else + torrent = (Snark)_snarks.get(filename); + remaining = _snarks.size(); + } + if (torrent != null) { + boolean wasStopped = torrent.stopped; + torrent.stopTorrent(); + if (remaining == 0) { + // should we disconnect/reconnect here (taking care to deal with the other thread's + // I2PServerSocket.accept() call properly?) + ////_util. + } + if (!wasStopped) + addMessage("Torrent stopped: '" + sfile.getName() + "'."); + } + return torrent; + } + /** + * Stop the torrent and delete the torrent file itself, but leaving the data + * behind. + */ + public void removeTorrent(String filename) { + Snark torrent = stopTorrent(filename, true); + if (torrent != null) { + File torrentFile = new File(filename); + torrentFile.delete(); + if (torrent.storage != null) + removeTorrentStatus(torrent.storage.getMetaInfo()); + addMessage("Torrent removed: '" + torrentFile.getName() + "'."); + } + } + + private class DirMonitor implements Runnable { + public void run() { + try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {} + // the first message was a "We are starting up in 1m" + synchronized (_messages) { + if (_messages.size() == 1) + _messages.remove(0); + } + + // here because we need to delay until I2CP is up + // although the user will see the default until then + getBWLimit(); + while (true) { + File dir = getDataDir(); + _log.debug("Directory Monitor loop over " + dir.getAbsolutePath()); + try { + monitorTorrents(dir); + } catch (Exception e) { + _log.error("Error in the DirectoryMonitor", e); + } + try { Thread.sleep(60*1000); } catch (InterruptedException ie) {} + } + } + } + + /** two listeners */ + public void torrentComplete(Snark snark) { + File f = new File(snark.torrent); + long len = snark.meta.getTotalLength(); + addMessage("Download finished: " + f.getName() + " (size: " + DataHelper.formatSize(len) + "B)"); + updateStatus(snark); + } + + public void updateStatus(Snark snark) { + saveTorrentStatus(snark.meta, snark.storage.getBitField()); + } + + private void monitorTorrents(File dir) { + String fileNames[] = dir.list(TorrentFilenameFilter.instance()); + List foundNames = new ArrayList(0); + if (fileNames != null) { + for (int i = 0; i < fileNames.length; i++) { + try { + foundNames.add(new File(dir, fileNames[i]).getCanonicalPath()); + } catch (IOException ioe) { + _log.error("Error resolving '" + fileNames[i] + "' in '" + dir, ioe); + } + } + } + + Set existingNames = listTorrentFiles(); + // lets find new ones first... + for (int i = 0; i < foundNames.size(); i++) { + if (existingNames.contains(foundNames.get(i))) { + // already known. noop + } else { + if (shouldAutoStart() && !_util.connect()) + addMessage("Unable to connect to I2P!"); + addTorrent((String)foundNames.get(i), !shouldAutoStart()); + } + } + // now lets see which ones have been removed... + for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + if (foundNames.contains(name)) { + // known and still there. noop + } else { + // known, but removed. drop it + stopTorrent(name, true); + } + } + } + + private static final String DEFAULT_TRACKERS[] = { +// "Postman", "http://YRgrgTLGnbTq2aZOZDJQ~o6Uk5k6TK-OZtx0St9pb0G-5EGYURZioxqYG8AQt~LgyyI~NCj6aYWpPO-150RcEvsfgXLR~CxkkZcVpgt6pns8SRc3Bi-QSAkXpJtloapRGcQfzTtwllokbdC-aMGpeDOjYLd8b5V9Im8wdCHYy7LRFxhEtGb~RL55DA8aYOgEXcTpr6RPPywbV~Qf3q5UK55el6Kex-6VCxreUnPEe4hmTAbqZNR7Fm0hpCiHKGoToRcygafpFqDw5frLXToYiqs9d4liyVB-BcOb0ihORbo0nS3CLmAwZGvdAP8BZ7cIYE3Z9IU9D1G8JCMxWarfKX1pix~6pIA-sp1gKlL1HhYhPMxwyxvuSqx34o3BqU7vdTYwWiLpGM~zU1~j9rHL7x60pVuYaXcFQDR4-QVy26b6Pt6BlAZoFmHhPcAuWfu-SFhjyZYsqzmEmHeYdAwa~HojSbofg0TMUgESRXMw6YThK1KXWeeJVeztGTz25sL8AAAA.i2p/announce.php=http://tracker.postman.i2p/" +// , "eBook", "http://E71FRom6PZNEqTN2Lr8P-sr23b7HJVC32KoGnVQjaX6zJiXwhJy2HsXob36Qmj81TYFZdewFZa9mSJ533UZgGyQkXo2ahctg82JKYZfDe5uDxAn1E9YPjxZCWJaFJh0S~UwSs~9AZ7UcauSJIoNtpxrtbmRNVFLqnkEDdLZi26TeucfOmiFmIWnVblLniWv3tG1boE9Abd-6j3FmYVrRucYuepAILYt6katmVNOk6sXmno1Eynrp~~MBuFq0Ko6~jsc2E2CRVYXDhGHEMdt-j6JUz5D7S2RIVzDRqQyAZLKJ7OdQDmI31przzmne1vOqqqLC~1xUumZVIvF~yOeJUGNjJ1Vx0J8i2BQIusn1pQJ6UCB~ZtZZLQtEb8EPVCfpeRi2ri1M5CyOuxN0V5ekmPHrYIBNevuTCRC26NP7ZS5VDgx1~NaC3A-CzJAE6f1QXi0wMI9aywNG5KGzOPifcsih8eyGyytvgLtrZtV7ykzYpPCS-rDfITncpn5hliPUAAAA.i2p/pub/bt/announce.php=http://de-ebook-archiv.i2p/pub/bt/" +// , "Gaytorrents", "http://uxPWHbK1OIj9HxquaXuhMiIvi21iK0~ZiG9d8G0840ZXIg0r6CbiV71xlsqmdnU6wm0T2LySriM0doW2gUigo-5BNkUquHwOjLROiETnB3ZR0Ml4IGa6QBPn1aAq2d9~g1r1nVjLE~pcFnXB~cNNS7kIhX1d6nLgYVZf0C2cZopEow2iWVUggGGnAA9mHjE86zLEnTvAyhbAMTqDQJhEuLa0ZYSORqzJDMkQt90MV4YMjX1ICY6RfUSFmxEqu0yWTrkHsTtRw48l~dz9wpIgc0a0T9C~eeWvmBFTqlJPtQZwntpNeH~jF7nlYzB58olgV2HHFYpVYD87DYNzTnmNWxCJ5AfDorm6AIUCV2qaE7tZtI1h6fbmGpGlPyW~Kw5GXrRfJwNvr6ajwAVi~bPVnrBwDZezHkfW4slOO8FACPR28EQvaTu9nwhAbqESxV2hCTq6vQSGjuxHeOuzBOEvRWkLKOHWTC09t2DbJ94FSqETmZopTB1ukEmaxRWbKSIaAAAA.i2p/announce.php=http://gaytorrents.i2p/" +// , "NickyB", "http://9On6d3cZ27JjwYCtyJJbowe054d5tFnfMjv4PHsYs-EQn4Y4mk2zRixatvuAyXz2MmRfXG-NAUfhKr0KCxRNZbvHmlckYfT-WBzwwpiMAl0wDFY~Pl8cqXuhfikSG5WrqdPfDNNIBuuznS0dqaczf~OyVaoEOpvuP3qV6wKqbSSLpjOwwAaQPHjlRtNIW8-EtUZp-I0LT45HSoowp~6b7zYmpIyoATvIP~sT0g0MTrczWhbVTUZnEkZeLhOR0Duw1-IRXI2KHPbA24wLO9LdpKKUXed05RTz0QklW5ROgR6TYv7aXFufX8kC0-DaKvQ5JKG~h8lcoHvm1RCzNqVE-2aiZnO2xH08H-iCWoLNJE-Td2kT-Tsc~3QdQcnEUcL5BF-VT~QYRld2--9r0gfGl-yDrJZrlrihHGr5J7ImahelNn9PpkVp6eIyABRmJHf2iicrk3CtjeG1j9OgTSwaNmEpUpn4aN7Kx0zNLdH7z6uTgCGD9Kmh1MFYrsoNlTp4AAAA.i2p/bittorrent/announce.php=http://nickyb.i2p/bittorrent/" +// , "Orion", "http://gKik1lMlRmuroXVGTZ~7v4Vez3L3ZSpddrGZBrxVriosCQf7iHu6CIk8t15BKsj~P0JJpxrofeuxtm7SCUAJEr0AIYSYw8XOmp35UfcRPQWyb1LsxUkMT4WqxAT3s1ClIICWlBu5An~q-Mm0VFlrYLIPBWlUFnfPR7jZ9uP5ZMSzTKSMYUWao3ejiykr~mtEmyls6g-ZbgKZawa9II4zjOy-hdxHgP-eXMDseFsrym4Gpxvy~3Fv9TuiSqhpgm~UeTo5YBfxn6~TahKtE~~sdCiSydqmKBhxAQ7uT9lda7xt96SS09OYMsIWxLeQUWhns-C~FjJPp1D~IuTrUpAFcVEGVL-BRMmdWbfOJEcWPZ~CBCQSO~VkuN1ebvIOr9JBerFMZSxZtFl8JwcrjCIBxeKPBmfh~xYh16BJm1BBBmN1fp2DKmZ2jBNkAmnUbjQOqWvUcehrykWk5lZbE7bjJMDFH48v3SXwRuDBiHZmSbsTY6zhGY~GkMQHNGxPMMSIAAAA.i2p/bt/announce.php=http://orion.i2p/bt/" +// , "anonymity", "http://8EoJZIKrWgGuDrxA3nRJs1jsPfiGwmFWL91hBrf0HA7oKhEvAna4Ocx47VLUR9retVEYBAyWFK-eZTPcvhnz9XffBEiJQQ~kFSCqb1fV6IfPiV3HySqi9U5Caf6~hC46fRd~vYnxmaBLICT3N160cxBETqH3v2rdxdJpvYt8q4nMk9LUeVXq7zqCTFLLG5ig1uKgNzBGe58iNcsvTEYlnbYcE930ABmrzj8G1qQSgSwJ6wx3tUQNl1z~4wSOUMan~raZQD60lRK70GISjoX0-D0Po9WmPveN3ES3g72TIET3zc3WPdK2~lgmKGIs8GgNLES1cXTolvbPhdZK1gxddRMbJl6Y6IPFyQ9o4-6Rt3Lp-RMRWZ2TG7j2OMcNSiOmATUhKEFBDfv-~SODDyopGBmfeLw16F4NnYednvn4qP10dyMHcUASU6Zag4mfc2-WivrOqeWhD16fVAh8MoDpIIT~0r9XmwdaVFyLcjbXObabJczxCAW3fodQUnvuSkwzAAAA.i2p/anonymityTracker/announce.php=http://anonymityweb.i2p/anonymityTracker/" +// , "The freak's tracker", "http://mHKva9x24E5Ygfey2llR1KyQHv5f8hhMpDMwJDg1U-hABpJ2NrQJd6azirdfaR0OKt4jDlmP2o4Qx0H598~AteyD~RJU~xcWYdcOE0dmJ2e9Y8-HY51ie0B1yD9FtIV72ZI-V3TzFDcs6nkdX9b81DwrAwwFzx0EfNvK1GLVWl59Ow85muoRTBA1q8SsZImxdyZ-TApTVlMYIQbdI4iQRwU9OmmtefrCe~ZOf4UBS9-KvNIqUL0XeBSqm0OU1jq-D10Ykg6KfqvuPnBYT1BYHFDQJXW5DdPKwcaQE4MtAdSGmj1epDoaEBUa9btQlFsM2l9Cyn1hzxqNWXELmx8dRlomQLlV4b586dRzW~fLlOPIGC13ntPXogvYvHVyEyptXkv890jC7DZNHyxZd5cyrKC36r9huKvhQAmNABT2Y~pOGwVrb~RpPwT0tBuPZ3lHYhBFYmD8y~AOhhNHKMLzea1rfwTvovBMByDdFps54gMN1mX4MbCGT4w70vIopS9yAAAA.i2p/bytemonsoon/announce.php" +// , "mastertracker", "http://VzXD~stRKbL3MOmeTn1iaCQ0CFyTmuFHiKYyo0Rd~dFPZFCYH-22rT8JD7i-C2xzYFa4jT5U2aqHzHI-Jre4HL3Ri5hFtZrLk2ax3ji7Qfb6qPnuYkuiF2E2UDmKUOppI8d9Ye7tjdhQVCy0izn55tBaB-U7UWdcvSK2i85sauyw3G0Gfads1Rvy5-CAe2paqyYATcDmGjpUNLoxbfv9KH1KmwRTNH6k1v4PyWYYnhbT39WfKMbBjSxVQRdi19cyJrULSWhjxaQfJHeWx5Z8Ev4bSPByBeQBFl2~4vqy0S5RypINsRSa3MZdbiAAyn5tr5slWR6QdoqY3qBQgBJFZppy-3iWkFqqKgSxCPundF8gdDLC5ddizl~KYcYKl42y9SGFHIukH-TZs8~em0~iahzsqWVRks3zRG~tlBcX2U3M2~OJs~C33-NKhyfZT7-XFBREvb8Szmd~p66jDxrwOnKaku-G6DyoQipJqIz4VHmY9-y5T8RrUcJcM-5lVoMpAAAA.i2p/announce.php=http://tracker.mastertracker.i2p/" +// , "Galen", "http://5jpwQMI5FT303YwKa5Rd38PYSX04pbIKgTaKQsWbqoWjIfoancFdWCShXHLI5G5ofOb0Xu11vl2VEMyPsg1jUFYSVnu4-VfMe3y4TKTR6DTpetWrnmEK6m2UXh91J5DZJAKlgmO7UdsFlBkQfR2rY853-DfbJtQIFl91tbsmjcA5CGQi4VxMFyIkBzv-pCsuLQiZqOwWasTlnzey8GcDAPG1LDcvfflGV~6F5no9mnuisZPteZKlrv~~TDoXTj74QjByWc4EOYlwqK8sbU9aOvz~s31XzErbPTfwiawiaZ0RUI-IDrKgyvmj0neuFTWgjRGVTH8bz7cBZIc3viy6ioD-eMQOrXaQL0TCWZUelRwHRvgdPiQrxdYQs7ixkajeHzxi-Pq0EMm5Vbh3j3Q9kfUFW3JjFDA-MLB4g6XnjCbM5J1rC0oOBDCIEfhQkszru5cyLjHiZ5yeA0VThgu~c7xKHybv~OMXION7V8pBKOgET7ZgAkw1xgYe3Kkyq5syAAAA.i2p/tr/announce.php=http://galen.i2p/tr/" + "POSTMAN", "http://tracker2.postman.i2p/announce.php=http://tracker2.postman.i2p/" + ,"WELTERDE", "http://BGKmlDOoH3RzFbPRfRpZV2FjpVj8~3moFftw5-dZfDf2070TOe8Tf2~DAVeaM6ZRLdmFEt~9wyFL8YMLMoLoiwGEH6IGW6rc45tstN68KsBDWZqkTohV1q9XFgK9JnCwE~Oi89xLBHsLMTHOabowWM6dkC8nI6QqJC2JODqLPIRfOVrDdkjLwtCrsckzLybNdFmgfoqF05UITDyczPsFVaHtpF1sRggOVmdvCM66otyonlzNcJbn59PA-R808vUrCPMGU~O9Wys0i-NoqtIbtWfOKnjCRFMNw5ex4n9m5Sxm9e20UkpKG6qzEuvKZWi8vTLe1NW~CBrj~vG7I3Ok4wybUFflBFOaBabxYJLlx4xTE1zJIVxlsekmAjckB4v-cQwulFeikR4LxPQ6mCQknW2HZ4JQIq6hL9AMabxjOlYnzh7kjOfRGkck8YgeozcyTvcDUcUsOuSTk06L4kdrv8h2Cozjbloi5zl6KTbj5ZTciKCxi73Pn9grICn-HQqEAAAA.i2p/a=http://tracker.welterde.i2p/stats?mode=top5" + , "CRSTRACK", "http://b4G9sCdtfvccMAXh~SaZrPqVQNyGQbhbYMbw6supq2XGzbjU4NcOmjFI0vxQ8w1L05twmkOvg5QERcX6Mi8NQrWnR0stLExu2LucUXg1aYjnggxIR8TIOGygZVIMV3STKH4UQXD--wz0BUrqaLxPhrm2Eh9Hwc8TdB6Na4ShQUq5Xm8D4elzNUVdpM~RtChEyJWuQvoGAHY3ppX-EJJLkiSr1t77neS4Lc-KofMVmgI9a2tSSpNAagBiNI6Ak9L1T0F9uxeDfEG9bBSQPNMOSUbAoEcNxtt7xOW~cNOAyMyGydwPMnrQ5kIYPY8Pd3XudEko970vE0D6gO19yoBMJpKx6Dh50DGgybLQ9CpRaynh2zPULTHxm8rneOGRcQo8D3mE7FQ92m54~SvfjXjD2TwAVGI~ae~n9HDxt8uxOecAAvjjJ3TD4XM63Q9TmB38RmGNzNLDBQMEmJFpqQU8YeuhnS54IVdUoVQFqui5SfDeLXlSkh4vYoMU66pvBfWbAAAA.i2p/tracker/announce.php=http://crstrack.i2p/tracker/" + + }; + + /** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */ + public static final String PROP_TRACKERS = "i2psnark.trackers"; + private static Map trackerMap = null; + /** sorted map of name to announceURL=baseURL */ + public Map getTrackers() { + if (trackerMap != null) // only do this once, can't be updated while running + return trackerMap; + Map rv = new TreeMap(); + String trackers = _config.getProperty(PROP_TRACKERS); + if ( (trackers == null) || (trackers.trim().length() <= 0) ) + trackers = _context.getProperty(PROP_TRACKERS); + if ( (trackers == null) || (trackers.trim().length() <= 0) ) { + for (int i = 0; i < DEFAULT_TRACKERS.length; i += 2) + rv.put(DEFAULT_TRACKERS[i], DEFAULT_TRACKERS[i+1]); + } else { + StringTokenizer tok = new StringTokenizer(trackers, ","); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split <= 0) + continue; + String name = pair.substring(0, split).trim(); + String url = pair.substring(split+1).trim(); + if ( (name.length() > 0) && (url.length() > 0) ) + rv.put(name, url); + } + } + + trackerMap = rv; + return trackerMap; + } + + private static class TorrentFilenameFilter implements FilenameFilter { + private static final TorrentFilenameFilter _filter = new TorrentFilenameFilter(); + public static TorrentFilenameFilter instance() { return _filter; } + public boolean accept(File dir, String name) { + return (name != null) && (name.endsWith(".torrent")); + } + } + + public class SnarkManagerShutdown extends I2PAppThread { + @Override + public void run() { + Set names = listTorrentFiles(); + for (Iterator iter = names.iterator(); iter.hasNext(); ) { + Snark snark = getTorrent((String)iter.next()); + if ( (snark != null) && (!snark.stopped) ) + snark.stopTorrent(); + } + } + } +}