diff --git a/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java b/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java index ff6454ee23..58a73b668f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java +++ b/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java @@ -66,7 +66,8 @@ class IdleChecker extends SimpleTimer2.TimedEvent { if (_log.shouldLog(Log.WARN)) _log.warn("Closing tunnels on idle"); _util.disconnect(); - _mgr.addMessage(_util.getString("I2P tunnel closed.")); + _mgr.addMessage(_util.getString("No more torrents running.") + ' ' + + _util.getString("I2P tunnel closed.")); schedule(3 * CHECK_TIME); return; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java index 35008394e8..cf295a530d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java @@ -163,7 +163,7 @@ class PeerState implements DataLoader _log.debug(peer + " rcv bitfield"); if (bitfield != null) { - // XXX - Be liberal in what you except? + // XXX - Be liberal in what you accept? if (_log.shouldLog(Log.WARN)) _log.warn("Got unexpected bitfield message from " + peer); return; diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index d57fc3c455..297f37c6b0 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -34,6 +34,7 @@ import net.i2p.I2PAppContext; import net.i2p.client.streaming.I2PServerSocket; import net.i2p.data.Destination; import net.i2p.util.Log; +import net.i2p.util.SecureFile; /** * Main Snark program startup class. @@ -221,7 +222,7 @@ public class Snark private PeerCoordinator coordinator; private ConnectionAcceptor acceptor; private TrackerClient trackerclient; - private String rootDataDir = "."; + private final File rootDataDir; private final CompleteListener completeListener; private volatile boolean stopped; private volatile boolean starting; @@ -238,13 +239,21 @@ public class Snark private volatile String activity = "Not started"; - /** from main() via parseArguments() single torrent */ + /** + * from main() via parseArguments() single torrent + * + * @deprecated unused + */ Snark(I2PSnarkUtil util, String torrent, String ip, int user_port, StorageListener slistener, CoordinatorListener clistener) { this(util, torrent, ip, user_port, slistener, clistener, null, null, null, true, "."); } - /** single torrent - via router */ + /** + * single torrent - via router + * + * @deprecated unused + */ public Snark(I2PAppContext ctx, Properties opts, String torrent, StorageListener slistener, boolean start, String rootDir) { this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir); @@ -275,11 +284,28 @@ public class Snark this.startTorrent(); } - /** multitorrent */ + /** + * multitorrent + */ public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port, StorageListener slistener, CoordinatorListener clistener, CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet, ConnectionAcceptor connectionAcceptor, boolean start, String rootDir) + { + this(util, torrent, ip, user_port, slistener, clistener, complistener, + peerCoordinatorSet, connectionAcceptor, start, rootDir, null); + } + + /** + * multitorrent + * + * @param baseFile if null, use rootDir/torrentName; if non-null, use it instead + * @since 0.9.11 + */ + public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port, + StorageListener slistener, CoordinatorListener clistener, + CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet, + ConnectionAcceptor connectionAcceptor, boolean start, String rootDir, File baseFile) { if (slistener == null) slistener = this; @@ -291,7 +317,7 @@ public class Snark acceptor = connectionAcceptor; this.torrent = torrent; - this.rootDataDir = rootDir; + this.rootDataDir = new File(rootDir); stopped = true; activity = "Network setup"; @@ -394,13 +420,19 @@ public class Snark try { activity = "Checking storage"; - storage = new Storage(_util, meta, slistener); + if (baseFile == null) { + String base = Storage.filterName(meta.getName()); + if (_util.getFilesPublic()) + baseFile = new File(rootDataDir, base); + else + baseFile = new SecureFile(rootDataDir, base); + } + storage = new Storage(_util, baseFile, meta, slistener); if (completeListener != null) { - storage.check(rootDataDir, - completeListener.getSavedTorrentTime(this), + storage.check(completeListener.getSavedTorrentTime(this), completeListener.getSavedTorrentBitField(this)); } else { - storage.check(rootDataDir); + storage.check(); } // have to figure out when to reopen // if (!start) @@ -452,7 +484,7 @@ public class Snark this.torrent = torrent; this.infoHash = ih; this.additionalTrackerURL = trackerURL; - this.rootDataDir = rootDir; + this.rootDataDir = new File(rootDir); stopped = true; id = generateID(); @@ -547,7 +579,7 @@ public class Snark } else if (trackerclient.halted()) { if (storage != null) { try { - storage.reopen(rootDataDir); + storage.reopen(); } catch (IOException ioe) { try { storage.close(); } catch (IOException ioee) { ioee.printStackTrace(); @@ -1102,9 +1134,15 @@ public class Snark */ public void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo) { try { + String base = Storage.filterName(metainfo.getName()); + File baseFile; + if (_util.getFilesPublic()) + baseFile = new File(rootDataDir, base); + else + baseFile = new SecureFile(rootDataDir, base); // The following two may throw IOE... - storage = new Storage(_util, metainfo, this); - storage.check(rootDataDir); + storage = new Storage(_util, baseFile, metainfo, this); + storage.check(); // ... so don't set meta until here meta = metainfo; if (completeListener != null) { diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index ba6733bcc6..3cb1db2fbb 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -15,6 +15,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; @@ -40,6 +41,7 @@ import net.i2p.util.SimpleTimer; import net.i2p.util.SimpleTimer2; import org.klomp.snark.dht.DHT; +import org.klomp.snark.dht.KRPC; /** * Manage multiple snarks @@ -55,7 +57,10 @@ public class SnarkManager implements CompleteListener { /** used to prevent DirMonitor from deleting torrents that don't have a torrent file yet */ private final Set _magnets; private final Object _addSnarkLock; - private /* FIXME final FIXME */ File _configFile; + private File _configFile; + private File _configDir; + /** one lock for all config, files for simplicity */ + private final Object _configLock = new Object(); private Properties _config; private final I2PAppContext _context; private final String _contextPath; @@ -81,14 +86,20 @@ public class SnarkManager implements CompleteListener { 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"; - public static final String PROP_META_PRIORITY_SUFFIX = ".priority"; - public static final String PROP_META_MAGNET_PREFIX = "i2psnark.magnet."; + private static final String PROP_META_PREFIX = "i2psnark.zmeta."; + private static final String PROP_META_STAMP = "stamp"; + private static final String PROP_META_BASE = "base"; + private static final String PROP_META_BITFIELD = "bitfield"; + private static final String PROP_META_PRIORITY = "priority"; + private static final String PROP_META_BITFIELD_SUFFIX = ".bitfield"; + private static final String PROP_META_PRIORITY_SUFFIX = ".priority"; + private static final String PROP_META_MAGNET_PREFIX = "i2psnark.magnet."; private static final String CONFIG_FILE_SUFFIX = ".config"; + private static final String CONFIG_FILE = "i2psnark" + CONFIG_FILE_SUFFIX; public static final String PROP_FILES_PUBLIC = "i2psnark.filesPublic"; - public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops + public static final String PROP_OLD_AUTO_START = "i2snark.autoStart"; // oops + public static final String PROP_AUTO_START = "i2psnark.autoStart"; // convert in migration to new config file 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:///"; @@ -109,6 +120,9 @@ public class SnarkManager implements CompleteListener { public static final int DEFAULT_STARTUP_DELAY = 3; public static final int DEFAULT_REFRESH_DELAY_SECS = 60; private static final int DEFAULT_PAGE_SIZE = 50; + public static final String CONFIG_DIR_SUFFIX = ".d"; + private static final String SUBDIR_PREFIX = "s"; + private static final String B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~"; /** * "name", "announceURL=websiteURL" pairs @@ -169,9 +183,11 @@ public class SnarkManager implements CompleteListener { _messages = new LinkedBlockingQueue(); _util = new I2PSnarkUtil(_context, ctxName); String cfile = ctxName + CONFIG_FILE_SUFFIX; - _configFile = new File(cfile); - if (!_configFile.isAbsolute()) - _configFile = new File(_context.getConfigDir(), cfile); + File configFile = new File(cfile); + if (!configFile.isAbsolute()) + configFile = new File(_context.getConfigDir(), cfile); + _configDir = migrateConfig(configFile); + _configFile = new File(_configDir, CONFIG_FILE); _trackerMap = new ConcurrentHashMap(4); loadConfig(null); } @@ -328,20 +344,179 @@ public class SnarkManager implements CompleteListener { return f; } + /** + * Migrate the old flat config file to the new config dir + * containing the config file minus the per-torrent entries, + * the dht file, and 16 subdirs for per-torrent config files + * Caller must synch. + * + * @return the new config directory, non-null + * @throws RuntimeException on creation fail + * @since 0.9.11 + */ + private File migrateConfig(File oldFile) { + File dir = new SecureDirectory(oldFile + CONFIG_DIR_SUFFIX); + if ((!dir.exists()) && (!dir.mkdirs())) { + _log.error("Error creating I2PSnark config dir " + dir); + throw new RuntimeException("Error creating I2PSnark config dir " + dir); + } + // move the DHT file as-is + String oldName = oldFile.toString(); + if (oldName.endsWith(CONFIG_FILE_SUFFIX)) { + String oldDHT = oldName.replace(CONFIG_FILE_SUFFIX, KRPC.DHT_FILE_SUFFIX); + File oldDHTFile = new File(oldDHT); + if (oldDHTFile.exists()) { + File newDHTFile = new File(dir, "i2psnark" + KRPC.DHT_FILE_SUFFIX); + FileUtil.rename(oldDHTFile, newDHTFile); + } + } + if (!oldFile.exists()) + return dir; + Properties oldProps = new Properties(); + try { + DataHelper.loadProps(oldProps, oldFile); + // a good time to fix this ancient typo + String auto = (String) oldProps.remove(PROP_OLD_AUTO_START); + if (auto != null) + oldProps.setProperty(PROP_AUTO_START, auto); + } catch (IOException ioe) { + _log.error("Error loading I2PSnark config " + oldFile, ioe); + return dir; + } + // Gather the props for each torrent, removing them from config + // old b64 of hash as key + Map configs = new HashMap(16); + for (Iterator> iter = oldProps.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry e = iter.next(); + String k = (String) e.getKey(); + if (k.startsWith(PROP_META_PREFIX)) { + iter.remove(); + String v = (String) e.getValue(); + try { + k = k.substring(PROP_META_PREFIX.length()); + String h = k.substring(0, 28); // length of b64 of 160 bit infohash + k = k.substring(29); // skip '.' + Properties tprops = configs.get(h); + if (tprops == null) { + tprops = new OrderedProperties(); + configs.put(h, tprops); + } + if (k.equals(PROP_META_BITFIELD)) { + // old config was timestamp,bitfield; split them + int comma = v.indexOf(','); + if (comma > 0 && v.length() > comma + 1) { + tprops.put(PROP_META_STAMP, v.substring(0, comma)); + tprops.put(PROP_META_BITFIELD, v.substring(comma + 1)); + } else { + // timestamp only?? + tprops.put(PROP_META_STAMP, v); + } + } else { + tprops.put(k, v); + } + } catch (IndexOutOfBoundsException ioobe) { + continue; + } + } + } + // Now make a config file for each torrent + for (Map.Entry e : configs.entrySet()) { + String b64 = e.getKey(); + Properties props = e.getValue(); + if (props.isEmpty()) + continue; + b64 = b64.replace('$', '='); + byte[] ih = Base64.decode(b64); + if (ih == null || ih.length != 20) + continue; + File cfg = configFile(dir, ih); + if (!cfg.exists()) { + File subdir = cfg.getParentFile(); + if (!subdir.exists()) + subdir.mkdirs(); + try { + DataHelper.storeProps(props, cfg); + } catch (IOException ioe) { + _log.error("Error storing I2PSnark config " + cfg, ioe); + } + } + } + // now store in new location, minus the zmeta entries + File newFile = new File(dir, CONFIG_FILE); + Properties newProps = new OrderedProperties(); + newProps.putAll(oldProps); + try { + DataHelper.storeProps(newProps, newFile); + } catch (IOException ioe) { + _log.error("Error storing I2PSnark config " + newFile, ioe); + return dir; + } + oldFile.delete(); + if (_log.shouldLog(Log.WARN)) + _log.warn("Config migrated from " + oldFile + " to " + dir); + return dir; + } + + /** + * The config for a torrent + * @return non-null, possibly empty + * @since 0.9.11 + */ + private Properties getConfig(Snark snark) { + return getConfig(snark.getInfoHash()); + } + + /** + * The config for a torrent + * @param ih 20-byte infohash + * @return non-null, possibly empty + * @since 0.9.11 + */ + private Properties getConfig(byte[] ih) { + Properties rv = new OrderedProperties(); + File conf = configFile(_configDir, ih); + synchronized(_configLock) { // one lock for all + try { + DataHelper.loadProps(rv, conf); + } catch (IOException ioe) {} + } + return rv; + } + + /** + * The config file for a torrent + * @param confDir the config directory + * @param ih 20-byte infohash + * @since 0.9.11 + */ + private static File configFile(File confDir, byte[] ih) { + String hex = I2PSnarkUtil.toHex(ih); + File subdir = new SecureDirectory(confDir, SUBDIR_PREFIX + B64.charAt((ih[0] >> 2) & 0x3f)); + return new File(subdir, hex + CONFIG_FILE_SUFFIX); + } + /** null to set initial defaults */ public void loadConfig(String filename) { + synchronized(_configLock) { + locked_loadConfig(filename); + } + } + + /** null to set initial defaults */ + private void locked_loadConfig(String filename) { if (_config == null) _config = new OrderedProperties(); if (filename != null) { File cfg = new File(filename); if (!cfg.isAbsolute()) cfg = new File(_context.getConfigDir(), filename); - _configFile = cfg; - if (cfg.exists()) { + _configDir = migrateConfig(cfg); + _configFile = new File(_configDir, CONFIG_FILE); + if (_configFile.exists()) { try { - DataHelper.loadProps(_config, cfg); + DataHelper.loadProps(_config, _configFile); } catch (IOException ioe) { - _log.error("Error loading I2PSnark config '" + filename + "'", ioe); + _log.error("Error loading I2PSnark config " + _configFile, ioe); } } } @@ -375,6 +550,7 @@ public class SnarkManager implements CompleteListener { // _config.setProperty(PROP_USE_DHT, Boolean.toString(I2PSnarkUtil.DEFAULT_USE_DHT)); updateConfig(); } + /** * Get current theme. * @return String -- the current theme @@ -492,6 +668,18 @@ public class SnarkManager implements CompleteListener { String startDelay, String pageSize, String seedPct, String eepHost, String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts, String upLimit, String upBW, boolean useOpenTrackers, boolean useDHT, String theme) { + synchronized(_configLock) { + locked_updateConfig(dataDir, filesPublic, autoStart, refreshDelay, + startDelay, pageSize, seedPct, eepHost, + eepPort, i2cpHost, i2cpPort, i2cpOpts, + upLimit, upBW, useOpenTrackers, useDHT, theme); + } + } + + private void locked_updateConfig(String dataDir, boolean filesPublic, boolean autoStart, String refreshDelay, + String startDelay, String pageSize, String seedPct, String eepHost, + String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts, + String upLimit, String upBW, boolean useOpenTrackers, boolean useDHT, String theme) { boolean changed = false; boolean interruptMonitor = false; //if (eepHost != null) { @@ -823,7 +1011,7 @@ public class SnarkManager implements CompleteListener { public void saveConfig() { try { - synchronized (_configFile) { + synchronized (_configLock) { DataHelper.storeProps(_config, _configFile); } } catch (IOException ioe) { @@ -831,13 +1019,6 @@ public class SnarkManager implements CompleteListener { } } - public Properties getConfig() { return _config; } - - /** @since Jetty 7 */ - public String getConfigFilename() { - return _configFile.getAbsolutePath(); - } - /** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */ public static final int MAX_FILES_PER_TORRENT = 512; @@ -895,15 +1076,23 @@ public class SnarkManager implements CompleteListener { /** * Caller must verify this torrent is not already added. + * + * @param filename the absolute path to save the metainfo to, generally ending in ".torrent" + * @param baseFile may be null, if so look in rootDataDir * @throws RuntimeException via Snark.fatal() */ - private void addTorrent(String filename) { addTorrent(filename, false); } + private void addTorrent(String filename) { + addTorrent(filename, null, false); + } /** * Caller must verify this torrent is not already added. + * + * @param filename the absolute path to save the metainfo to, generally ending in ".torrent" + * @param baseFile may be null, if so look in rootDataDir * @throws RuntimeException via Snark.fatal() */ - private void addTorrent(String filename, boolean dontAutoStart) { + private void addTorrent(String filename, File baseFile, boolean dontAutoStart) { if ((!dontAutoStart) && !_util.connected()) { addMessage(_("Connecting to I2P")); boolean ok = _util.connect(); @@ -984,9 +1173,13 @@ public class SnarkManager implements CompleteListener { } else { // TODO load saved closest DHT nodes and pass to the Snark ? // This may take a LONG time + if (baseFile == null) + baseFile = getSavedBaseFile(info.getInfoHash()); + if (_log.shouldLog(Log.INFO)) + _log.info("New Snark, torrent: " + filename + " base: " + baseFile); torrent = new Snark(_util, filename, null, -1, null, null, this, _peerCoordinatorSet, _connectionAcceptor, - false, dataDir.getPath()); + false, dataDir.getPath(), baseFile); loadSavedFilePriorities(torrent); synchronized (_snarks) { _snarks.put(filename, torrent); @@ -1129,14 +1322,17 @@ public class SnarkManager implements CompleteListener { * This verifies that a torrent with this infohash is not already added. * This may take a LONG time to create or check the storage. * + * Called from servlet. + * * @param metainfo the metainfo for the torrent * @param bitfield the current completion status of the torrent * @param filename the absolute path to save the metainfo to, generally ending in ".torrent", which is also the name of the torrent * Must be a filesystem-safe name. + * @param baseFile may be null, if so look in rootDataDir * @throws RuntimeException via Snark.fatal() * @since 0.8.4 */ - public void addTorrent(MetaInfo metainfo, BitField bitfield, String filename, boolean dontAutoStart) throws IOException { + public void addTorrent(MetaInfo metainfo, BitField bitfield, String filename, File baseFile, boolean dontAutoStart) throws IOException { // prevent interference by DirMonitor synchronized (_snarks) { Snark snark = getTorrentByInfoHash(metainfo.getInfoHash()); @@ -1145,11 +1341,11 @@ public class SnarkManager implements CompleteListener { return; } // so addTorrent won't recheck - saveTorrentStatus(metainfo, bitfield, null); // no file priorities + saveTorrentStatus(metainfo, bitfield, null, baseFile); // no file priorities try { locked_writeMetaInfo(metainfo, filename, areFilesPublic()); // hold the lock for a long time - addTorrent(filename, dontAutoStart); + addTorrent(filename, baseFile, dontAutoStart); } catch (IOException ioe) { addMessage(_("Failed to copy torrent file to {0}", filename)); _log.error("Failed to write torrent file", ioe); @@ -1222,16 +1418,10 @@ public class SnarkManager implements CompleteListener { * A Snark.CompleteListener method. */ public long getSavedTorrentTime(Snark snark) { - byte[] ih = snark.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + Properties config = getConfig(snark); + String time = config.getProperty(PROP_META_STAMP); 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; } @@ -1245,16 +1435,10 @@ public class SnarkManager implements CompleteListener { MetaInfo metainfo = snark.getMetaInfo(); if (metainfo == null) return null; - byte[] ih = snark.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + Properties config = getConfig(snark); + String bf = config.getProperty(PROP_META_BITFIELD); 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); @@ -1281,10 +1465,8 @@ public class SnarkManager implements CompleteListener { return; if (metainfo.getFiles() == null) return; - byte[] ih = snark.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String pri = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX); + Properties config = getConfig(snark); + String pri = config.getProperty(PROP_META_PRIORITY); if (pri == null) return; int filecount = metainfo.getFiles().size(); @@ -1299,24 +1481,39 @@ public class SnarkManager implements CompleteListener { } storage.setFilePriorities(rv); } + + /** + * Get the base location for a torrent from the config file. + * @return File or null, doesn't necessarily exist + * @since 0.9.11 + */ + public File getSavedBaseFile(byte[] ih) { + Properties config = getConfig(ih); + String base = config.getProperty(PROP_META_BASE); + if (base == null) + return null; + return new File(base); + } /** * 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 '='. + * for that torrent. * 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. * * @param bitfield non-null * @param priorities may be null + * @param base may be null */ - public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) { + public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, File base) { + synchronized (_configLock) { + locked_saveTorrentStatus(metainfo, bitfield, priorities, base); + } + } + + private void locked_saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, File base) { byte[] ih = metainfo.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String now = "" + System.currentTimeMillis(); String bfs; if (bitfield.complete()) { bfs = "."; @@ -1324,10 +1521,13 @@ public class SnarkManager implements CompleteListener { byte[] bf = bitfield.getFieldBytes(); bfs = Base64.encode(bf); } - _config.setProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX, now + "," + bfs); + Properties config = getConfig(ih); + config.setProperty(PROP_META_STAMP, Long.toString(System.currentTimeMillis())); + config.setProperty(PROP_META_BITFIELD, bfs); + if (base != null) + config.setProperty(PROP_META_BASE, base.getAbsolutePath()); // now the file priorities - String prop = PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX; if (priorities != null) { boolean nonzero = false; for (int i = 0; i < priorities.length; i++) { @@ -1345,30 +1545,40 @@ public class SnarkManager implements CompleteListener { if (i != priorities.length - 1) buf.append(','); } - _config.setProperty(prop, buf.toString()); + config.setProperty(PROP_META_PRIORITY, buf.toString()); } else { - _config.remove(prop); + config.remove(PROP_META_PRIORITY); } } else { - _config.remove(prop); + config.remove(PROP_META_PRIORITY); } // TODO save closest DHT nodes too - saveConfig(); + File conf = configFile(_configDir, ih); + File subdir = conf.getParentFile(); + if (!subdir.exists()) + subdir.mkdirs(); + try { + DataHelper.storeProps(config, conf); + } catch (IOException ioe) { + _log.error("Unable to save the config to " + conf); + } } /** - * Remove the status of a torrent from the config file. - * This may help the config file from growing too big. + * Remove the status of a torrent by removing the config file. */ 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); - _config.remove(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX); - saveConfig(); + File conf = configFile(_configDir, ih); + synchronized (_configLock) { + conf.delete(); + File subdir = conf.getParentFile(); + String[] files = subdir.list(); + if (files != null && files.length == 0) + subdir.delete(); + } } /** @@ -1568,7 +1778,7 @@ public class SnarkManager implements CompleteListener { MetaInfo meta = snark.getMetaInfo(); Storage storage = snark.getStorage(); if (meta != null && storage != null) - saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities()); + saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities(), storage.getBase()); } /** @@ -1590,7 +1800,7 @@ public class SnarkManager implements CompleteListener { snark.stopTorrent(); return null; } - saveTorrentStatus(meta, storage.getBitField(), null); // no file priorities + saveTorrentStatus(meta, storage.getBitField(), null, storage.getBase()); // no file priorities // temp for addMessage() in case canonical throws String name = storage.getBaseName(); try { @@ -1691,7 +1901,7 @@ public class SnarkManager implements CompleteListener { try { // Snark.fatal() throws a RuntimeException // don't let one bad torrent kill the whole loop - addTorrent(name, !shouldAutoStart()); + addTorrent(name, null, !shouldAutoStart()); } catch (Exception e) { addMessage(_("Error: Could not add the torrent {0}", name) + ": " + e); _log.error("Unable to add the torrent " + name, e); diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java index 5b7aa962fb..abb80bd942 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java @@ -32,7 +32,9 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.SortedSet; import java.util.StringTokenizer; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -52,6 +54,7 @@ public class Storage { private final MetaInfo metainfo; private final List _torrentFiles; + private final File _base; private final StorageListener listener; private final I2PSnarkUtil _util; private final Log _log; @@ -83,15 +86,18 @@ public class Storage private static final ByteCache _cache = ByteCache.getInstance(16, BUFSIZE); /** - * Creates a new storage based on the supplied MetaInfo. This will + * Creates a new storage based on the supplied MetaInfo. + * + * Does not check storage. Caller MUST call check(), which will * try to create and/or check all needed files in the MetaInfo. * - * Does not check storage. Caller MUST call check() + * @param baseFile the torrent data file or dir */ - public Storage(I2PSnarkUtil util, MetaInfo metainfo, StorageListener listener) + public Storage(I2PSnarkUtil util, File baseFile, MetaInfo metainfo, StorageListener listener) { _util = util; _log = util.getContext().logManager().getLog(Storage.class); + _base = baseFile; this.metainfo = metainfo; this.listener = listener; needed = metainfo.getPieces(); @@ -121,6 +127,7 @@ public class Storage throws IOException { _util = util; + _base = baseFile; _log = util.getContext().logManager().getLog(Storage.class); this.listener = listener; // Create names, rafs and lengths arrays. @@ -305,24 +312,15 @@ public class Storage } /** - * @param file canonical path (non-directory) + * @param file non-canonical path (non-directory) * @return number of bytes remaining; -1 if unknown file * @since 0.7.14 */ - public long remaining(String file) { + public long remaining(File file) { long bytes = 0; for (TorrentFile tf : _torrentFiles) { File f = tf.RAFfile; - // use canonical in case snark dir or sub dirs are symlinked - String canonical = null; - if (f != null) { - try { - canonical = f.getCanonicalPath(); - } catch (IOException ioe) { - f = null; - } - } - if (f != null && canonical.equals(file)) { + if (f.equals(file)) { if (complete()) return 0; int psz = piece_size; @@ -348,22 +346,16 @@ public class Storage } /** - * @param file canonical path (non-directory) + * @param file non-canonical path (non-directory) * @since 0.8.1 */ - public int getPriority(String file) { + public int getPriority(File file) { if (complete() || metainfo.getFiles() == null) return 0; for (TorrentFile tf : _torrentFiles) { File f = tf.RAFfile; - // use canonical in case snark dir or sub dirs are symlinked - if (f != null) { - try { - String canonical = f.getCanonicalPath(); - if (canonical.equals(file)) - return tf.priority; - } catch (IOException ioe) {} - } + if (f.equals(file)) + return tf.priority; } return 0; } @@ -371,24 +363,18 @@ public class Storage /** * Must call Snark.updatePiecePriorities() * (which calls getPiecePriorities()) after calling this. - * @param file canonical path (non-directory) + * @param file non-canonical path (non-directory) * @param pri default 0; <0 to disable * @since 0.8.1 */ - public void setPriority(String file, int pri) { + public void setPriority(File file, int pri) { if (complete() || metainfo.getFiles() == null) return; for (TorrentFile tf : _torrentFiles) { File f = tf.RAFfile; - // use canonical in case snark dir or sub dirs are symlinked - if (f != null) { - try { - String canonical = f.getCanonicalPath(); - if (canonical.equals(file)) { - tf.priority = pri; - return; - } - } catch (IOException ioe) {} + if (f.equals(file)) { + tf.priority = pri; + return; } } } @@ -490,9 +476,9 @@ public class Storage * Creates (and/or checks) all files from the metainfo file list. * Only call this once, and only after the constructor with the metainfo. */ - public void check(String rootDir) throws IOException + public void check() throws IOException { - check(rootDir, 0, null); + check(0, null); } /** @@ -500,14 +486,9 @@ public class Storage * Use a saved bitfield and timestamp from a config file. * Only call this once, and only after the constructor with the metainfo. */ - public void check(String rootDir, long savedTime, BitField savedBitField) throws IOException + public void check(long savedTime, BitField savedBitField) throws IOException { - File base; boolean areFilesPublic = _util.getFilesPublic(); - if (areFilesPublic) - base = new File(rootDir, filterName(metainfo.getName())); - else - base = new SecureFile(rootDir, filterName(metainfo.getName())); boolean useSavedBitField = savedTime > 0 && savedBitField != null; if (!_torrentFiles.isEmpty()) @@ -523,12 +504,12 @@ public class Storage if (!base.exists() && !base.createNewFile()) throw new IOException("Could not create file " + base); - _torrentFiles.add(new TorrentFile(base, base, metainfo.getTotalLength())); + _torrentFiles.add(new TorrentFile(_base, _base, metainfo.getTotalLength())); if (useSavedBitField) { - long lm = base.lastModified(); + long lm = _base.lastModified(); if (lm <= 0 || lm > savedTime) useSavedBitField = false; - else if (base.length() != metainfo.getTotalLength()) + else if (_base.length() != metainfo.getTotalLength()) useSavedBitField = false; } } @@ -536,9 +517,9 @@ public class Storage { // Create base as dir. if (_log.shouldLog(Log.INFO)) - _log.info("Creating/Checking directory: " + base); - if (!base.mkdir() && !base.isDirectory()) - throw new IOException("Could not create directory " + base); + _log.info("Creating/Checking directory: " + _base); + if (!_base.mkdir() && !_base.isDirectory()) + throw new IOException("Could not create directory " + _base); List ls = metainfo.getLengths(); int size = files.size(); @@ -546,7 +527,7 @@ public class Storage for (int i = 0; i < size; i++) { List path = files.get(i); - File f = createFileFromNames(base, path, areFilesPublic); + File f = createFileFromNames(_base, path, areFilesPublic); // dup file name check after filtering for (int j = 0; j < i; j++) { if (f.equals(_torrentFiles.get(j).RAFfile)) { @@ -562,12 +543,12 @@ public class Storage else lastPath = '_' + lastPath; path.set(last, lastPath); - f = createFileFromNames(base, path, areFilesPublic); + f = createFileFromNames(_base, path, areFilesPublic); j = 0; } } long len = ls.get(i).longValue(); - _torrentFiles.add(new TorrentFile(base, f, len)); + _torrentFiles.add(new TorrentFile(_base, f, len)); total += len; if (useSavedBitField) { long lm = f.lastModified(); @@ -614,7 +595,7 @@ public class Storage * @param rootDir ignored * @throws IOE on fail */ - public void reopen(String rootDir) throws IOException + public void reopen() throws IOException { if (_torrentFiles.isEmpty()) throw new IOException("Storage not checked yet"); @@ -690,6 +671,8 @@ public class Storage * Note that filtering each path element individually may lead to * things going in the wrong place if there are duplicates * in intermediate path elements after filtering. + * + * @param names path elements */ private static File createFileFromNames(File base, List names, boolean areFilesPublic) throws IOException { @@ -725,15 +708,47 @@ public class Storage return f; } - public static File getFileFromNames(File base, List names) - { - Iterator it = names.iterator(); - while (it.hasNext()) - { - String name = filterName(it.next()); - base = new File(base, name); + /** + * The base file or directory. + * @return the File + * @since 0.9.11 + */ + public File getBase() { + return _base; + } + + /** + * Does not include directories. Unsorted. + * @return a new List + * @since 0.9.11 + */ + public List getFiles() { + List rv = new ArrayList(_torrentFiles.size()); + for (TorrentFile tf : _torrentFiles) { + rv.add(tf.RAFfile); } - return base; + return rv; + } + + /** + * Includes the base for a multi-file torrent. + * Sorted bottom-up for easy deletion. + * Slow. Use for deletion only. + * @since 0.9.11 + * @return a new Set or null for a single-file torrent + */ + public SortedSet getDirectories() { + if (!_base.isDirectory()) + return null; + SortedSet rv = new TreeSet(Collections.reverseOrder()); + rv.add(_base); + for (TorrentFile tf : _torrentFiles) { + File f = tf.RAFfile; + do { + f = f.getParentFile(); + } while (f != null && rv.add(f)); + } + return rv; } /** diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index 50c3a0a00a..85a0807f51 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -40,6 +40,7 @@ import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.SimpleTimer2; +import org.klomp.snark.SnarkManager; import org.klomp.snark.TrackerClient; import org.klomp.snark.bencode.BDecoder; import org.klomp.snark.bencode.BEncoder; @@ -152,7 +153,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private static final long CLEAN_TIME = 63*1000; private static final long EXPLORE_TIME = 877*1000; private static final long BLACKLIST_CLEAN_TIME = 17*60*1000; - private static final String DHT_FILE_SUFFIX = ".dht.dat"; + public static final String DHT_FILE_SUFFIX = ".dht.dat"; private static final int SEND_CRYPTO_TAGS = 8; private static final int LOW_CRYPTO_TAGS = 4; @@ -185,8 +186,14 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _myNID = new NID(_myID); } _myNodeInfo = new NodeInfo(_myNID, session.getMyDestination(), _qPort); - _dhtFile = new File(ctx.getConfigDir(), baseName + DHT_FILE_SUFFIX); - _backupDhtFile = baseName.equals("i2psnark") ? null : new File(ctx.getConfigDir(), "i2psnark" + DHT_FILE_SUFFIX); + File conf = new File(ctx.getConfigDir(), baseName + ".config" + SnarkManager.CONFIG_DIR_SUFFIX); + _dhtFile = new File(conf, "i2psnark" + DHT_FILE_SUFFIX); + if (baseName.equals("i2psnark")) { + _backupDhtFile = null; + } else { + File bconf = new File(ctx.getConfigDir(), "i2psnark.config" + SnarkManager.CONFIG_DIR_SUFFIX); + _backupDhtFile = new File(bconf, "i2psnark" + DHT_FILE_SUFFIX); + } _knownNodes = new DHTNodes(ctx, _myNID); start(); diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index ae39a7937b..6b6d62a408 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -18,7 +18,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; -import java.util.TreeSet; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -190,7 +189,8 @@ public class I2PSnarkServlet extends BasicServlet { 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") || path.equals("/_post") || isConfigure)) { + if (!(path == null || path.equals("/") || path.equals("/index.jsp") || + path.equals("/index.html") || path.equals("/_post") || isConfigure)) { if (path.endsWith("/")) { // Listing of a torrent (torrent detail page) // bypass the horrid Resource.getListHTML() @@ -840,42 +840,36 @@ public class I2PSnarkServlet extends BasicServlet { _manager.addMessage(_("Data file could not be deleted: {0}", f.getAbsolutePath())); break; } + Storage storage = snark.getStorage(); + if (storage == null) + break; // step 1 delete files - for (int i = 0; i < files.size(); i++) { - // 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, files.get(i)); + for (File df : storage.getFiles()) { if (df.delete()) { //_manager.addMessage(_("Data file deleted: {0}", df.getAbsolutePath())); } else { _manager.addMessage(_("Data file could not be deleted: {0}", df.getAbsolutePath())); } } - // step 2 make Set of dirs with reverse sort - Set dirs = new TreeSet(Collections.reverseOrder()); - for (List list : files) { - for (int i = 1; i < list.size(); i++) { - dirs.add(Storage.getFileFromNames(f, list.subList(0, i))); - } - } - // step 3 delete dirs bottom-up + // step 2 delete dirs bottom-up + Set dirs = storage.getDirectories(); + if (_log.shouldLog(Log.INFO)) + _log.info("Dirs to delete: " + DataHelper.toString(dirs)); + boolean ok = false; for (File df : dirs) { if (df.delete()) { + ok = true; //_manager.addMessage(_("Data dir deleted: {0}", df.getAbsolutePath())); } else { + ok = false; _manager.addMessage(_("Directory could not be deleted: {0}", df.getAbsolutePath())); if (_log.shouldLog(Log.WARN)) _log.warn("Could not delete dir " + df); } } - // step 4 delete base - if (f.delete()) { - _manager.addMessage(_("Directory deleted: {0}", f.getAbsolutePath())); - } else { - _manager.addMessage(_("Directory could not be deleted: {0}", f.getAbsolutePath())); - if (_log.shouldLog(Log.WARN)) - _log.warn("Could not delete dir " + f); - } + // step 3 message for base (last one) + if (ok) + _manager.addMessage(_("Directory deleted: {0}", storage.getBase())); break; } } @@ -914,7 +908,9 @@ public class I2PSnarkServlet extends BasicServlet { } else if ("Create".equals(action)) { String baseData = req.getParameter("baseFile"); if (baseData != null && baseData.trim().length() > 0) { - File baseFile = new File(_manager.getDataDir(), baseData); + File baseFile = new File(baseData.trim()); + if (!baseFile.isAbsolute()) + baseFile = new File(_manager.getDataDir(), baseData); String announceURL = req.getParameter("announceURL"); // make the user add a tracker on the config form now //String announceURLOther = req.getParameter("announceURLOther"); @@ -974,7 +970,7 @@ public class I2PSnarkServlet extends BasicServlet { File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent"); // FIXME is the storage going to stay around thanks to the info reference? // now add it, but don't automatically start it - _manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), true); + _manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), baseFile, true); _manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath()); if (announceURL != null && !_manager.util().getOpenTrackers().contains(announceURL)) _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName())); @@ -1775,10 +1771,11 @@ public class I2PSnarkServlet extends BasicServlet { out.write("
\n
"); //out.write("From file:
\n"); out.write(_("Data to seed")); - out.write(":
" + _manager.getDataDir().getAbsolutePath() + File.separatorChar - + "" + + "
\n"); out.write(_("Trackers")); out.write(":
"); @@ -2221,9 +2218,11 @@ public class I2PSnarkServlet extends BasicServlet { private static class ListingComparator implements Comparator, Serializable { public int compare(File l, File r) { - if (l.isDirectory() && !r.isDirectory()) + boolean ld = l.isDirectory(); + boolean rd = r.isDirectory(); + if (ld && !rd) return -1; - if (r.isDirectory() && !l.isDirectory()) + if (rd && !ld) return 1; return Collator.getInstance().compare(l.getName(), r.getName()); } @@ -2262,12 +2261,6 @@ public class I2PSnarkServlet extends BasicServlet { private String getListHTML(File r, String base, boolean parent, Map postParams) throws IOException { - File[] ls = null; - if (r.isDirectory()) { - ls = r.listFiles(); - Arrays.sort(ls, new ListingComparator()); - } // if r is not a directory, we are only showing torrent info section - String title = decodePath(base); String cpath = _contextPath + '/'; if (title.startsWith(cpath)) @@ -2284,7 +2277,14 @@ public class I2PSnarkServlet extends BasicServlet { if (snark != null && postParams != null) { // caller must P-R-G - savePriorities(snark, postParams); + String[] val = postParams.get("nonce"); + if (val != null) { + String nonce = val[0]; + if (String.valueOf(_nonce).equals(nonce)) + savePriorities(snark, postParams); + else + _manager.addMessage("Please retry form submission (bad nonce)"); + } return null; } @@ -2297,7 +2297,7 @@ public class I2PSnarkServlet extends BasicServlet { buf.append(title); buf.append("").append(HEADER_A).append(_themePath).append(HEADER_B).append("" + "\n
\"\"  "); + buf.append(" class=\"snarkRefresh\">\"\"  "); if (_contextName.equals(DEFAULT_NAME)) buf.append(_("I2PSnark")); else @@ -2306,9 +2306,12 @@ public class I2PSnarkServlet extends BasicServlet { if (parent) // always true buf.append("
"); - boolean showPriority = ls != null && snark != null && snark.getStorage() != null && !snark.getStorage().complete(); - if (showPriority) + boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() && + r.isDirectory(); + if (showPriority) { buf.append("
\n"); + buf.append("\n"); + } if (snark != null) { // first table - torrent info buf.append("\n"); @@ -2321,11 +2324,17 @@ public class I2PSnarkServlet extends BasicServlet { String fullPath = snark.getName(); String baseName = urlEncode((new File(fullPath)).getName()); buf.append("\n"); + buf.append("\n"); String announce = null; MetaInfo meta = snark.getMetaInfo(); @@ -2426,40 +2435,40 @@ public class I2PSnarkServlet extends BasicServlet { // .append(MAGGOT).append(hex).append(':').append(hex).append(""); buf.append("\n"); } buf.append("
") - .append("\"\" ") + .append("\"\" ") .append(_("Torrent file")) .append(": ") .append(fullPath) .append("
") + .append("\"\" ") + .append(_("Data location")) + .append(": ") + .append(urlEncode(snark.getStorage().getBase().getPath())) + .append("
") - .append("\"\" ") + .append("\"\" ") .append(_("Size")) .append(": ") .append(formatSize(snark.getTotalLength())); int pieces = snark.getPieces(); double completion = (pieces - snark.getNeeded()) / (double) pieces; if (completion < 1.0) - buf.append(" \"\" ") + buf.append(" \"\" ") .append(_("Completion")) .append(": ") .append((new DecimalFormat("0.00%")).format(completion)); else - buf.append(" \"\" ") + buf.append(" \"\" ") .append(_("Complete")); // else unknown long needed = snark.getNeededLength(); if (needed > 0) - buf.append(" \"\" ") + buf.append(" \"\" ") .append(_("Remaining")) .append(": ") .append(formatSize(needed)); if (meta != null) { List> files = meta.getFiles(); int fileCount = files != null ? files.size() : 1; - buf.append(" \"\" ") + buf.append(" \"\" ") .append(_("Files")) .append(": ") .append(fileCount); } - buf.append(" \"\" ") + buf.append(" \"\" ") .append(_("Pieces")) .append(": ") .append(pieces); - buf.append(" \"\" ") + buf.append(" \"\" ") .append(_("Piece size")) .append(": ") .append(formatSize(snark.getPieceLength(0))) @@ -2472,6 +2481,22 @@ public class I2PSnarkServlet extends BasicServlet { .append("\"
\n"); + + if (snark != null && !r.exists()) { + // fixup TODO + buf.append("

Does not exist
resource=\"").append(r.toString()) + .append("\"
base=\"").append(base) + .append("\"
torrent=\"").append(torrentName) + .append("\"

"); + return buf.toString(); + } + + File[] ls = null; + if (r.isDirectory()) { + ls = r.listFiles(); + Arrays.sort(ls, new ListingComparator()); + } // if r is not a directory, we are only showing torrent info section + if (ls == null) { // We are only showing the torrent info section buf.append("
"); @@ -2482,7 +2507,7 @@ public class I2PSnarkServlet extends BasicServlet { buf.append("\n"); buf.append("\n") .append("\n"); buf.append("\n"); buf.append("\n"); if (showPriority) buf.append("\n"); buf.append("\n"); @@ -2535,6 +2560,7 @@ public class I2PSnarkServlet extends BasicServlet { boolean complete = false; String status = ""; long length = item.length(); + int priority = 0; if (item.isDirectory()) { complete = true; //status = toImg("tick") + ' ' + _("Directory"); @@ -2545,9 +2571,8 @@ public class I2PSnarkServlet extends BasicServlet { status = toImg("cancel") + ' ' + _("Torrent not found?"); } else { Storage storage = snark.getStorage(); - try { - File f = item; - long remaining = storage.remaining(f.getCanonicalPath()); + + long remaining = storage.remaining(item); if (remaining < 0) { complete = true; status = toImg("cancel") + ' ' + _("File not found in torrent?"); @@ -2555,7 +2580,7 @@ public class I2PSnarkServlet extends BasicServlet { complete = true; status = toImg("tick") + ' ' + _("Complete"); } else { - int priority = storage.getPriority(f.getCanonicalPath()); + priority = storage.getPriority(item); if (priority < 0) status = toImg("cancel"); else if (priority == 0) @@ -2566,9 +2591,7 @@ public class I2PSnarkServlet extends BasicServlet { (100 * (length - remaining) / length) + "% " + _("complete") + " (" + DataHelper.formatSize2(remaining) + "B " + _("remaining") + ")"; } - } catch (IOException ioe) { - status = "Not a file? " + ioe; - } + } } @@ -2608,21 +2631,19 @@ public class I2PSnarkServlet extends BasicServlet { buf.append(""); if (showPriority) { buf.append("
") - .append("") - .append("\"")") - .append("\"")") - .append("\"")\n
\"\" ") + buf.append("\">\"\" ") .append(_("Up to higher level directory")) .append("
"); - File f = item; if ((!complete) && (!item.isDirectory())) { - int pri = snark.getStorage().getPriority(f.getCanonicalPath()); - buf.append(" 0) + buf.append(" 0) buf.append("checked=\"true\""); buf.append('>').append(_("High")); - buf.append("').append(_("Normal")); - buf.append("').append(_("Skip")); showSaveButton = true; @@ -2694,6 +2715,8 @@ public class I2PSnarkServlet extends BasicServlet { icon = "application"; else if (plc.endsWith(".iso")) icon = "cd"; + else if (mime.equals("application/x-bittorrent")) + icon = "magnet"; else icon = "page_white"; return icon; @@ -2718,7 +2741,7 @@ public class I2PSnarkServlet extends BasicServlet { String key = entry.getKey(); if (key.startsWith("pri.")) { try { - String file = key.substring(4); + File file = new File(key.substring(4)); String val = entry.getValue()[0]; // jetty arrays int pri = Integer.parseInt(val); storage.setPriority(file, pri); @@ -2727,6 +2750,6 @@ public class I2PSnarkServlet extends BasicServlet { } } snark.updatePiecePriorities(); - _manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities()); + _manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities(), storage.getBase()); } } diff --git a/apps/i2psnark/mime.properties b/apps/i2psnark/mime.properties index 927155d9a7..b251fb72ea 100644 --- a/apps/i2psnark/mime.properties +++ b/apps/i2psnark/mime.properties @@ -24,6 +24,7 @@ su2 = application/zip su3 = application/zip sud = application/zip tbz = application/x-bzip2 +torrent = application/x-bittorrent txt = text/plain war = application/java-archive webm = video/webm