propagate from branch 'i2p.i2p.zzz.snarkconfig' (head ad48ab1a9e769c58ea2e286337927f5c0e1568be)

to branch 'i2p.i2p' (head 0cd9e265bd38c40839e68de8f51233489acad346)
This commit is contained in:
zzz
2014-08-10 14:14:00 +00:00
9 changed files with 577 additions and 235 deletions

View File

@ -66,7 +66,8 @@ class IdleChecker extends SimpleTimer2.TimedEvent {
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("Closing tunnels on idle"); _log.warn("Closing tunnels on idle");
_util.disconnect(); _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); schedule(3 * CHECK_TIME);
return; return;
} }

View File

@ -163,7 +163,7 @@ class PeerState implements DataLoader
_log.debug(peer + " rcv bitfield"); _log.debug(peer + " rcv bitfield");
if (bitfield != null) if (bitfield != null)
{ {
// XXX - Be liberal in what you except? // XXX - Be liberal in what you accept?
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("Got unexpected bitfield message from " + peer); _log.warn("Got unexpected bitfield message from " + peer);
return; return;

View File

@ -34,6 +34,7 @@ import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PServerSocket; import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.data.Destination; import net.i2p.data.Destination;
import net.i2p.util.Log; import net.i2p.util.Log;
import net.i2p.util.SecureFile;
/** /**
* Main Snark program startup class. * Main Snark program startup class.
@ -221,7 +222,7 @@ public class Snark
private PeerCoordinator coordinator; private PeerCoordinator coordinator;
private ConnectionAcceptor acceptor; private ConnectionAcceptor acceptor;
private TrackerClient trackerclient; private TrackerClient trackerclient;
private String rootDataDir = "."; private final File rootDataDir;
private final CompleteListener completeListener; private final CompleteListener completeListener;
private volatile boolean stopped; private volatile boolean stopped;
private volatile boolean starting; private volatile boolean starting;
@ -238,13 +239,21 @@ public class Snark
private volatile String activity = "Not started"; 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, Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
StorageListener slistener, CoordinatorListener clistener) { StorageListener slistener, CoordinatorListener clistener) {
this(util, torrent, ip, user_port, slistener, clistener, null, null, null, true, "."); 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, public Snark(I2PAppContext ctx, Properties opts, String torrent,
StorageListener slistener, boolean start, String rootDir) { StorageListener slistener, boolean start, String rootDir) {
this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir); this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir);
@ -275,11 +284,28 @@ public class Snark
this.startTorrent(); this.startTorrent();
} }
/** multitorrent */ /**
* multitorrent
*/
public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port, public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
StorageListener slistener, CoordinatorListener clistener, StorageListener slistener, CoordinatorListener clistener,
CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet, CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
ConnectionAcceptor connectionAcceptor, boolean start, String rootDir) 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) if (slistener == null)
slistener = this; slistener = this;
@ -291,7 +317,7 @@ public class Snark
acceptor = connectionAcceptor; acceptor = connectionAcceptor;
this.torrent = torrent; this.torrent = torrent;
this.rootDataDir = rootDir; this.rootDataDir = new File(rootDir);
stopped = true; stopped = true;
activity = "Network setup"; activity = "Network setup";
@ -394,13 +420,19 @@ public class Snark
try try
{ {
activity = "Checking storage"; 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) { if (completeListener != null) {
storage.check(rootDataDir, storage.check(completeListener.getSavedTorrentTime(this),
completeListener.getSavedTorrentTime(this),
completeListener.getSavedTorrentBitField(this)); completeListener.getSavedTorrentBitField(this));
} else { } else {
storage.check(rootDataDir); storage.check();
} }
// have to figure out when to reopen // have to figure out when to reopen
// if (!start) // if (!start)
@ -452,7 +484,7 @@ public class Snark
this.torrent = torrent; this.torrent = torrent;
this.infoHash = ih; this.infoHash = ih;
this.additionalTrackerURL = trackerURL; this.additionalTrackerURL = trackerURL;
this.rootDataDir = rootDir; this.rootDataDir = new File(rootDir);
stopped = true; stopped = true;
id = generateID(); id = generateID();
@ -547,7 +579,7 @@ public class Snark
} else if (trackerclient.halted()) { } else if (trackerclient.halted()) {
if (storage != null) { if (storage != null) {
try { try {
storage.reopen(rootDataDir); storage.reopen();
} catch (IOException ioe) { } catch (IOException ioe) {
try { storage.close(); } catch (IOException ioee) { try { storage.close(); } catch (IOException ioee) {
ioee.printStackTrace(); ioee.printStackTrace();
@ -1102,9 +1134,15 @@ public class Snark
*/ */
public void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo) { public void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo) {
try { 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... // The following two may throw IOE...
storage = new Storage(_util, metainfo, this); storage = new Storage(_util, baseFile, metainfo, this);
storage.check(rootDataDir); storage.check();
// ... so don't set meta until here // ... so don't set meta until here
meta = metainfo; meta = metainfo;
if (completeListener != null) { if (completeListener != null) {

View File

@ -15,6 +15,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
@ -40,6 +41,7 @@ import net.i2p.util.SimpleTimer;
import net.i2p.util.SimpleTimer2; import net.i2p.util.SimpleTimer2;
import org.klomp.snark.dht.DHT; import org.klomp.snark.dht.DHT;
import org.klomp.snark.dht.KRPC;
/** /**
* Manage multiple snarks * 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 */ /** used to prevent DirMonitor from deleting torrents that don't have a torrent file yet */
private final Set<String> _magnets; private final Set<String> _magnets;
private final Object _addSnarkLock; 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 Properties _config;
private final I2PAppContext _context; private final I2PAppContext _context;
private final String _contextPath; 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_UPLOADERS_TOTAL = "i2psnark.uploaders.total";
public static final String PROP_UPBW_MAX = "i2psnark.upbw.max"; public static final String PROP_UPBW_MAX = "i2psnark.upbw.max";
public static final String PROP_DIR = "i2psnark.dir"; public static final String PROP_DIR = "i2psnark.dir";
public static final String PROP_META_PREFIX = "i2psnark.zmeta."; private static final String PROP_META_PREFIX = "i2psnark.zmeta.";
public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield"; private static final String PROP_META_STAMP = "stamp";
public static final String PROP_META_PRIORITY_SUFFIX = ".priority"; private static final String PROP_META_BASE = "base";
public static final String PROP_META_MAGNET_PREFIX = "i2psnark.magnet."; 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_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_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 DEFAULT_AUTO_START = "false";
//public static final String PROP_LINK_PREFIX = "i2psnark.linkPrefix"; //public static final String PROP_LINK_PREFIX = "i2psnark.linkPrefix";
//public static final String DEFAULT_LINK_PREFIX = "file:///"; //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_STARTUP_DELAY = 3;
public static final int DEFAULT_REFRESH_DELAY_SECS = 60; public static final int DEFAULT_REFRESH_DELAY_SECS = 60;
private static final int DEFAULT_PAGE_SIZE = 50; 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 * "name", "announceURL=websiteURL" pairs
@ -169,9 +183,11 @@ public class SnarkManager implements CompleteListener {
_messages = new LinkedBlockingQueue<String>(); _messages = new LinkedBlockingQueue<String>();
_util = new I2PSnarkUtil(_context, ctxName); _util = new I2PSnarkUtil(_context, ctxName);
String cfile = ctxName + CONFIG_FILE_SUFFIX; String cfile = ctxName + CONFIG_FILE_SUFFIX;
_configFile = new File(cfile); File configFile = new File(cfile);
if (!_configFile.isAbsolute()) if (!configFile.isAbsolute())
_configFile = new File(_context.getConfigDir(), cfile); configFile = new File(_context.getConfigDir(), cfile);
_configDir = migrateConfig(configFile);
_configFile = new File(_configDir, CONFIG_FILE);
_trackerMap = new ConcurrentHashMap<String, Tracker>(4); _trackerMap = new ConcurrentHashMap<String, Tracker>(4);
loadConfig(null); loadConfig(null);
} }
@ -341,20 +357,179 @@ public class SnarkManager implements CompleteListener {
return f; 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<String, Properties> configs = new HashMap<String, Properties>(16);
for (Iterator<Map.Entry<Object, Object>> iter = oldProps.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry<Object, Object> 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<String, Properties> 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 */ /** null to set initial defaults */
public void loadConfig(String filename) { public void loadConfig(String filename) {
synchronized(_configLock) {
locked_loadConfig(filename);
}
}
/** null to set initial defaults */
private void locked_loadConfig(String filename) {
if (_config == null) if (_config == null)
_config = new OrderedProperties(); _config = new OrderedProperties();
if (filename != null) { if (filename != null) {
File cfg = new File(filename); File cfg = new File(filename);
if (!cfg.isAbsolute()) if (!cfg.isAbsolute())
cfg = new File(_context.getConfigDir(), filename); cfg = new File(_context.getConfigDir(), filename);
_configFile = cfg; _configDir = migrateConfig(cfg);
if (cfg.exists()) { _configFile = new File(_configDir, CONFIG_FILE);
if (_configFile.exists()) {
try { try {
DataHelper.loadProps(_config, cfg); DataHelper.loadProps(_config, _configFile);
} catch (IOException ioe) { } catch (IOException ioe) {
_log.error("Error loading I2PSnark config '" + filename + "'", ioe); _log.error("Error loading I2PSnark config " + _configFile, ioe);
} }
} }
} }
@ -388,6 +563,7 @@ public class SnarkManager implements CompleteListener {
// _config.setProperty(PROP_USE_DHT, Boolean.toString(I2PSnarkUtil.DEFAULT_USE_DHT)); // _config.setProperty(PROP_USE_DHT, Boolean.toString(I2PSnarkUtil.DEFAULT_USE_DHT));
updateConfig(); updateConfig();
} }
/** /**
* Get current theme. * Get current theme.
* @return String -- the current theme * @return String -- the current theme
@ -505,6 +681,18 @@ public class SnarkManager implements CompleteListener {
String startDelay, String pageSize, String seedPct, String eepHost, String startDelay, String pageSize, String seedPct, String eepHost,
String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts, String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts,
String upLimit, String upBW, boolean useOpenTrackers, boolean useDHT, String theme) { 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 changed = false;
boolean interruptMonitor = false; boolean interruptMonitor = false;
//if (eepHost != null) { //if (eepHost != null) {
@ -836,7 +1024,7 @@ public class SnarkManager implements CompleteListener {
public void saveConfig() { public void saveConfig() {
try { try {
synchronized (_configFile) { synchronized (_configLock) {
DataHelper.storeProps(_config, _configFile); DataHelper.storeProps(_config, _configFile);
} }
} catch (IOException ioe) { } catch (IOException ioe) {
@ -844,13 +1032,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. */ /** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */
public static final int MAX_FILES_PER_TORRENT = 512; public static final int MAX_FILES_PER_TORRENT = 512;
@ -908,15 +1089,23 @@ public class SnarkManager implements CompleteListener {
/** /**
* Caller must verify this torrent is not already added. * 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() * @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. * 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() * @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()) { if ((!dontAutoStart) && !_util.connected()) {
addMessage(_("Connecting to I2P")); addMessage(_("Connecting to I2P"));
boolean ok = _util.connect(); boolean ok = _util.connect();
@ -997,9 +1186,13 @@ public class SnarkManager implements CompleteListener {
} else { } else {
// TODO load saved closest DHT nodes and pass to the Snark ? // TODO load saved closest DHT nodes and pass to the Snark ?
// This may take a LONG time // 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, torrent = new Snark(_util, filename, null, -1, null, null, this,
_peerCoordinatorSet, _connectionAcceptor, _peerCoordinatorSet, _connectionAcceptor,
false, dataDir.getPath()); false, dataDir.getPath(), baseFile);
loadSavedFilePriorities(torrent); loadSavedFilePriorities(torrent);
synchronized (_snarks) { synchronized (_snarks) {
_snarks.put(filename, torrent); _snarks.put(filename, torrent);
@ -1142,14 +1335,17 @@ public class SnarkManager implements CompleteListener {
* This verifies that a torrent with this infohash is not already added. * This verifies that a torrent with this infohash is not already added.
* This may take a LONG time to create or check the storage. * This may take a LONG time to create or check the storage.
* *
* Called from servlet.
*
* @param metainfo the metainfo for the torrent * @param metainfo the metainfo for the torrent
* @param bitfield the current completion status of 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 * @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. * Must be a filesystem-safe name.
* @param baseFile may be null, if so look in rootDataDir
* @throws RuntimeException via Snark.fatal() * @throws RuntimeException via Snark.fatal()
* @since 0.8.4 * @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 // prevent interference by DirMonitor
synchronized (_snarks) { synchronized (_snarks) {
Snark snark = getTorrentByInfoHash(metainfo.getInfoHash()); Snark snark = getTorrentByInfoHash(metainfo.getInfoHash());
@ -1158,11 +1354,11 @@ public class SnarkManager implements CompleteListener {
return; return;
} }
// so addTorrent won't recheck // so addTorrent won't recheck
saveTorrentStatus(metainfo, bitfield, null); // no file priorities saveTorrentStatus(metainfo, bitfield, null, baseFile); // no file priorities
try { try {
locked_writeMetaInfo(metainfo, filename, areFilesPublic()); locked_writeMetaInfo(metainfo, filename, areFilesPublic());
// hold the lock for a long time // hold the lock for a long time
addTorrent(filename, dontAutoStart); addTorrent(filename, baseFile, dontAutoStart);
} catch (IOException ioe) { } catch (IOException ioe) {
addMessage(_("Failed to copy torrent file to {0}", filename)); addMessage(_("Failed to copy torrent file to {0}", filename));
_log.error("Failed to write torrent file", ioe); _log.error("Failed to write torrent file", ioe);
@ -1235,16 +1431,10 @@ public class SnarkManager implements CompleteListener {
* A Snark.CompleteListener method. * A Snark.CompleteListener method.
*/ */
public long getSavedTorrentTime(Snark snark) { public long getSavedTorrentTime(Snark snark) {
byte[] ih = snark.getInfoHash(); Properties config = getConfig(snark);
String infohash = Base64.encode(ih); String time = config.getProperty(PROP_META_STAMP);
infohash = infohash.replace('=', '$');
String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
if (time == null) if (time == null)
return 0; return 0;
int comma = time.indexOf(',');
if (comma <= 0)
return 0;
time = time.substring(0, comma);
try { return Long.parseLong(time); } catch (NumberFormatException nfe) {} try { return Long.parseLong(time); } catch (NumberFormatException nfe) {}
return 0; return 0;
} }
@ -1258,16 +1448,10 @@ public class SnarkManager implements CompleteListener {
MetaInfo metainfo = snark.getMetaInfo(); MetaInfo metainfo = snark.getMetaInfo();
if (metainfo == null) if (metainfo == null)
return null; return null;
byte[] ih = snark.getInfoHash(); Properties config = getConfig(snark);
String infohash = Base64.encode(ih); String bf = config.getProperty(PROP_META_BITFIELD);
infohash = infohash.replace('=', '$');
String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
if (bf == null) if (bf == null)
return null; return null;
int comma = bf.indexOf(',');
if (comma <= 0)
return null;
bf = bf.substring(comma + 1).trim();
int len = metainfo.getPieces(); int len = metainfo.getPieces();
if (bf.equals(".")) { if (bf.equals(".")) {
BitField bitfield = new BitField(len); BitField bitfield = new BitField(len);
@ -1294,10 +1478,8 @@ public class SnarkManager implements CompleteListener {
return; return;
if (metainfo.getFiles() == null) if (metainfo.getFiles() == null)
return; return;
byte[] ih = snark.getInfoHash(); Properties config = getConfig(snark);
String infohash = Base64.encode(ih); String pri = config.getProperty(PROP_META_PRIORITY);
infohash = infohash.replace('=', '$');
String pri = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX);
if (pri == null) if (pri == null)
return; return;
int filecount = metainfo.getFiles().size(); int filecount = metainfo.getFiles().size();
@ -1313,23 +1495,38 @@ public class SnarkManager implements CompleteListener {
storage.setFilePriorities(rv); 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 * Save the completion status of a torrent and the current time in the config file
* in the form "i2psnark.zmeta.$base64infohash=$time,$base64bitfield". * for that torrent.
* 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 time is a standard long converted to string.
* The status is either a bitfield converted to Base64 or "." for a completed * The status is either a bitfield converted to Base64 or "." for a completed
* torrent to save space in the config file and in memory. * torrent to save space in the config file and in memory.
* *
* @param bitfield non-null * @param bitfield non-null
* @param priorities may be 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(); byte[] ih = metainfo.getInfoHash();
String infohash = Base64.encode(ih);
infohash = infohash.replace('=', '$');
String now = "" + System.currentTimeMillis();
String bfs; String bfs;
if (bitfield.complete()) { if (bitfield.complete()) {
bfs = "."; bfs = ".";
@ -1337,10 +1534,13 @@ public class SnarkManager implements CompleteListener {
byte[] bf = bitfield.getFieldBytes(); byte[] bf = bitfield.getFieldBytes();
bfs = Base64.encode(bf); 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 // now the file priorities
String prop = PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX;
if (priorities != null) { if (priorities != null) {
boolean nonzero = false; boolean nonzero = false;
for (int i = 0; i < priorities.length; i++) { for (int i = 0; i < priorities.length; i++) {
@ -1358,30 +1558,40 @@ public class SnarkManager implements CompleteListener {
if (i != priorities.length - 1) if (i != priorities.length - 1)
buf.append(','); buf.append(',');
} }
_config.setProperty(prop, buf.toString()); config.setProperty(PROP_META_PRIORITY, buf.toString());
} else { } else {
_config.remove(prop); config.remove(PROP_META_PRIORITY);
} }
} else { } else {
_config.remove(prop); config.remove(PROP_META_PRIORITY);
} }
// TODO save closest DHT nodes too // 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. * Remove the status of a torrent by removing the config file.
* This may help the config file from growing too big.
*/ */
public void removeTorrentStatus(MetaInfo metainfo) { public void removeTorrentStatus(MetaInfo metainfo) {
byte[] ih = metainfo.getInfoHash(); byte[] ih = metainfo.getInfoHash();
String infohash = Base64.encode(ih); File conf = configFile(_configDir, ih);
infohash = infohash.replace('=', '$'); synchronized (_configLock) {
_config.remove(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); conf.delete();
_config.remove(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX); File subdir = conf.getParentFile();
saveConfig(); String[] files = subdir.list();
if (files != null && files.length == 0)
subdir.delete();
}
} }
/** /**
@ -1581,7 +1791,7 @@ public class SnarkManager implements CompleteListener {
MetaInfo meta = snark.getMetaInfo(); MetaInfo meta = snark.getMetaInfo();
Storage storage = snark.getStorage(); Storage storage = snark.getStorage();
if (meta != null && storage != null) if (meta != null && storage != null)
saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities()); saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities(), storage.getBase());
} }
/** /**
@ -1603,7 +1813,7 @@ public class SnarkManager implements CompleteListener {
snark.stopTorrent(); snark.stopTorrent();
return null; 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 // temp for addMessage() in case canonical throws
String name = storage.getBaseName(); String name = storage.getBaseName();
try { try {
@ -1704,7 +1914,7 @@ public class SnarkManager implements CompleteListener {
try { try {
// Snark.fatal() throws a RuntimeException // Snark.fatal() throws a RuntimeException
// don't let one bad torrent kill the whole loop // don't let one bad torrent kill the whole loop
addTorrent(name, !shouldAutoStart()); addTorrent(name, null, !shouldAutoStart());
} catch (Exception e) { } catch (Exception e) {
addMessage(_("Error: Could not add the torrent {0}", name) + ": " + e); addMessage(_("Error: Could not add the torrent {0}", name) + ": " + e);
_log.error("Unable to add the torrent " + name, e); _log.error("Unable to add the torrent " + name, e);

View File

@ -32,7 +32,9 @@ import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.SortedSet;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -52,6 +54,7 @@ public class Storage
{ {
private final MetaInfo metainfo; private final MetaInfo metainfo;
private final List<TorrentFile> _torrentFiles; private final List<TorrentFile> _torrentFiles;
private final File _base;
private final StorageListener listener; private final StorageListener listener;
private final I2PSnarkUtil _util; private final I2PSnarkUtil _util;
private final Log _log; private final Log _log;
@ -83,15 +86,18 @@ public class Storage
private static final ByteCache _cache = ByteCache.getInstance(16, BUFSIZE); 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. * 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; _util = util;
_log = util.getContext().logManager().getLog(Storage.class); _log = util.getContext().logManager().getLog(Storage.class);
_base = baseFile;
this.metainfo = metainfo; this.metainfo = metainfo;
this.listener = listener; this.listener = listener;
needed = metainfo.getPieces(); needed = metainfo.getPieces();
@ -121,6 +127,7 @@ public class Storage
throws IOException throws IOException
{ {
_util = util; _util = util;
_base = baseFile;
_log = util.getContext().logManager().getLog(Storage.class); _log = util.getContext().logManager().getLog(Storage.class);
this.listener = listener; this.listener = listener;
// Create names, rafs and lengths arrays. // 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 * @return number of bytes remaining; -1 if unknown file
* @since 0.7.14 * @since 0.7.14
*/ */
public long remaining(String file) { public long remaining(File file) {
long bytes = 0; long bytes = 0;
for (TorrentFile tf : _torrentFiles) { for (TorrentFile tf : _torrentFiles) {
File f = tf.RAFfile; File f = tf.RAFfile;
// use canonical in case snark dir or sub dirs are symlinked if (f.equals(file)) {
String canonical = null;
if (f != null) {
try {
canonical = f.getCanonicalPath();
} catch (IOException ioe) {
f = null;
}
}
if (f != null && canonical.equals(file)) {
if (complete()) if (complete())
return 0; return 0;
int psz = piece_size; 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 * @since 0.8.1
*/ */
public int getPriority(String file) { public int getPriority(File file) {
if (complete() || metainfo.getFiles() == null) if (complete() || metainfo.getFiles() == null)
return 0; return 0;
for (TorrentFile tf : _torrentFiles) { for (TorrentFile tf : _torrentFiles) {
File f = tf.RAFfile; File f = tf.RAFfile;
// use canonical in case snark dir or sub dirs are symlinked if (f.equals(file))
if (f != null) {
try {
String canonical = f.getCanonicalPath();
if (canonical.equals(file))
return tf.priority; return tf.priority;
} catch (IOException ioe) {}
}
} }
return 0; return 0;
} }
@ -371,25 +363,19 @@ public class Storage
/** /**
* Must call Snark.updatePiecePriorities() * Must call Snark.updatePiecePriorities()
* (which calls getPiecePriorities()) after calling this. * (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 * @param pri default 0; <0 to disable
* @since 0.8.1 * @since 0.8.1
*/ */
public void setPriority(String file, int pri) { public void setPriority(File file, int pri) {
if (complete() || metainfo.getFiles() == null) if (complete() || metainfo.getFiles() == null)
return; return;
for (TorrentFile tf : _torrentFiles) { for (TorrentFile tf : _torrentFiles) {
File f = tf.RAFfile; File f = tf.RAFfile;
// use canonical in case snark dir or sub dirs are symlinked if (f.equals(file)) {
if (f != null) {
try {
String canonical = f.getCanonicalPath();
if (canonical.equals(file)) {
tf.priority = pri; tf.priority = pri;
return; return;
} }
} catch (IOException ioe) {}
}
} }
} }
@ -490,9 +476,9 @@ public class Storage
* Creates (and/or checks) all files from the metainfo file list. * Creates (and/or checks) all files from the metainfo file list.
* Only call this once, and only after the constructor with the metainfo. * 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. * Use a saved bitfield and timestamp from a config file.
* Only call this once, and only after the constructor with the metainfo. * 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(); 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; boolean useSavedBitField = savedTime > 0 && savedBitField != null;
if (!_torrentFiles.isEmpty()) if (!_torrentFiles.isEmpty())
@ -517,18 +498,18 @@ public class Storage
{ {
// Create base as file. // Create base as file.
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info("Creating/Checking file: " + base); _log.info("Creating/Checking file: " + _base);
// createNewFile() can throw a "Permission denied" IOE even if the file exists??? // createNewFile() can throw a "Permission denied" IOE even if the file exists???
// so do it second // so do it second
if (!base.exists() && !base.createNewFile()) if (!_base.exists() && !_base.createNewFile())
throw new IOException("Could not create file " + base); 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) { if (useSavedBitField) {
long lm = base.lastModified(); long lm = _base.lastModified();
if (lm <= 0 || lm > savedTime) if (lm <= 0 || lm > savedTime)
useSavedBitField = false; useSavedBitField = false;
else if (base.length() != metainfo.getTotalLength()) else if (_base.length() != metainfo.getTotalLength())
useSavedBitField = false; useSavedBitField = false;
} }
} }
@ -536,9 +517,9 @@ public class Storage
{ {
// Create base as dir. // Create base as dir.
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info("Creating/Checking directory: " + base); _log.info("Creating/Checking directory: " + _base);
if (!base.mkdir() && !base.isDirectory()) if (!_base.mkdir() && !_base.isDirectory())
throw new IOException("Could not create directory " + base); throw new IOException("Could not create directory " + _base);
List<Long> ls = metainfo.getLengths(); List<Long> ls = metainfo.getLengths();
int size = files.size(); int size = files.size();
@ -546,7 +527,7 @@ public class Storage
for (int i = 0; i < size; i++) for (int i = 0; i < size; i++)
{ {
List<String> path = files.get(i); List<String> path = files.get(i);
File f = createFileFromNames(base, path, areFilesPublic); File f = createFileFromNames(_base, path, areFilesPublic);
// dup file name check after filtering // dup file name check after filtering
for (int j = 0; j < i; j++) { for (int j = 0; j < i; j++) {
if (f.equals(_torrentFiles.get(j).RAFfile)) { if (f.equals(_torrentFiles.get(j).RAFfile)) {
@ -562,12 +543,12 @@ public class Storage
else else
lastPath = '_' + lastPath; lastPath = '_' + lastPath;
path.set(last, lastPath); path.set(last, lastPath);
f = createFileFromNames(base, path, areFilesPublic); f = createFileFromNames(_base, path, areFilesPublic);
j = 0; j = 0;
} }
} }
long len = ls.get(i).longValue(); long len = ls.get(i).longValue();
_torrentFiles.add(new TorrentFile(base, f, len)); _torrentFiles.add(new TorrentFile(_base, f, len));
total += len; total += len;
if (useSavedBitField) { if (useSavedBitField) {
long lm = f.lastModified(); long lm = f.lastModified();
@ -614,7 +595,7 @@ public class Storage
* @param rootDir ignored * @param rootDir ignored
* @throws IOE on fail * @throws IOE on fail
*/ */
public void reopen(String rootDir) throws IOException public void reopen() throws IOException
{ {
if (_torrentFiles.isEmpty()) if (_torrentFiles.isEmpty())
throw new IOException("Storage not checked yet"); throw new IOException("Storage not checked yet");
@ -690,6 +671,8 @@ public class Storage
* Note that filtering each path element individually may lead to * Note that filtering each path element individually may lead to
* things going in the wrong place if there are duplicates * things going in the wrong place if there are duplicates
* in intermediate path elements after filtering. * in intermediate path elements after filtering.
*
* @param names path elements
*/ */
private static File createFileFromNames(File base, List<String> names, boolean areFilesPublic) throws IOException private static File createFileFromNames(File base, List<String> names, boolean areFilesPublic) throws IOException
{ {
@ -725,15 +708,47 @@ public class Storage
return f; return f;
} }
public static File getFileFromNames(File base, List<String> names) /**
{ * The base file or directory.
Iterator<String> it = names.iterator(); * @return the File
while (it.hasNext()) * @since 0.9.11
{ */
String name = filterName(it.next()); public File getBase() {
base = new File(base, name); return _base;
} }
return base;
/**
* Does not include directories. Unsorted.
* @return a new List
* @since 0.9.11
*/
public List<File> getFiles() {
List<File> rv = new ArrayList<File>(_torrentFiles.size());
for (TorrentFile tf : _torrentFiles) {
rv.add(tf.RAFfile);
}
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<File> getDirectories() {
if (!_base.isDirectory())
return null;
SortedSet<File> rv = new TreeSet<File>(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;
} }
/** /**

View File

@ -40,6 +40,7 @@ import net.i2p.util.I2PAppThread;
import net.i2p.util.Log; import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2; import net.i2p.util.SimpleTimer2;
import org.klomp.snark.SnarkManager;
import org.klomp.snark.TrackerClient; import org.klomp.snark.TrackerClient;
import org.klomp.snark.bencode.BDecoder; import org.klomp.snark.bencode.BDecoder;
import org.klomp.snark.bencode.BEncoder; 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 CLEAN_TIME = 63*1000;
private static final long EXPLORE_TIME = 877*1000; private static final long EXPLORE_TIME = 877*1000;
private static final long BLACKLIST_CLEAN_TIME = 17*60*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 SEND_CRYPTO_TAGS = 8;
private static final int LOW_CRYPTO_TAGS = 4; private static final int LOW_CRYPTO_TAGS = 4;
@ -185,8 +186,14 @@ public class KRPC implements I2PSessionMuxedListener, DHT {
_myNID = new NID(_myID); _myNID = new NID(_myID);
} }
_myNodeInfo = new NodeInfo(_myNID, session.getMyDestination(), _qPort); _myNodeInfo = new NodeInfo(_myNID, session.getMyDestination(), _qPort);
_dhtFile = new File(ctx.getConfigDir(), baseName + DHT_FILE_SUFFIX); File conf = new File(ctx.getConfigDir(), baseName + ".config" + SnarkManager.CONFIG_DIR_SUFFIX);
_backupDhtFile = baseName.equals("i2psnark") ? null : new File(ctx.getConfigDir(), "i2psnark" + DHT_FILE_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); _knownNodes = new DHTNodes(ctx, _myNID);
start(); start();

View File

@ -181,11 +181,10 @@ class BasicServlet extends HttpServlet
HttpContent r = null; HttpContent r = null;
if (_warBase != null && pathInContext.startsWith(_warBase)) { if (_warBase != null && pathInContext.startsWith(_warBase)) {
r = new JarContent(pathInContext); r = new JarContent(pathInContext);
} else if (!pathInContext.contains("..") && } else {
!pathInContext.endsWith("/")) { File f = getResource(pathInContext);
File f = new File(_resourceBase, pathInContext);
// exists && !directory // exists && !directory
if (f.isFile()) if (f != null && f.isFile())
r = new FileContent(f); r = new FileContent(f);
} }
return r; return r;

View File

@ -18,7 +18,6 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.TreeSet;
import javax.servlet.ServletConfig; import javax.servlet.ServletConfig;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -59,6 +58,7 @@ public class I2PSnarkServlet extends BasicServlet {
private static final String DEFAULT_NAME = "i2psnark"; private static final String DEFAULT_NAME = "i2psnark";
public static final String PROP_CONFIG_FILE = "i2psnark.configFile"; public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
private static final String WARBASE = "/.icons/";
public I2PSnarkServlet() { public I2PSnarkServlet() {
super(); super();
@ -84,7 +84,7 @@ public class I2PSnarkServlet extends BasicServlet {
_manager.start(); _manager.start();
loadMimeMap("org/klomp/snark/web/mime"); loadMimeMap("org/klomp/snark/web/mime");
setResourceBase(_manager.getDataDir()); setResourceBase(_manager.getDataDir());
setWarBase("/.icons/"); setWarBase(WARBASE);
} }
@Override @Override
@ -95,17 +95,35 @@ public class I2PSnarkServlet extends BasicServlet {
} }
/** /**
* We override this instead of passing a resource base to super(), because * We override this to set the file relative to the storage dirctory
* if a resource base is set, super.getResource() always uses that base, * for the torrent.
* and we can't get any resources (like icons) out of the .war *
* @param pathInContext should always start with /
*/ */
@Override @Override
public File getResource(String pathInContext) public File getResource(String pathInContext)
{ {
if (pathInContext == null || pathInContext.equals("/") || pathInContext.equals("/index.jsp") || if (pathInContext == null || pathInContext.equals("/") || pathInContext.equals("/index.jsp") ||
pathInContext.equals("/index.html") || pathInContext.startsWith("/.icons/")) !pathInContext.startsWith("/") || pathInContext.length() == 0 ||
pathInContext.equals("/index.html") || pathInContext.startsWith(WARBASE))
return super.getResource(pathInContext); return super.getResource(pathInContext);
// files in the i2psnark/ directory // files in the i2psnark/ directory
// get top level
pathInContext = pathInContext.substring(1);
File top = new File(pathInContext);
File parent;
while ((parent = top.getParentFile()) != null) {
top = parent;
}
Snark snark = _manager.getTorrentByBaseName(top.getPath());
if (snark != null) {
Storage storage = snark.getStorage();
if (storage != null) {
File sbase = storage.getBase();
String child = pathInContext.substring(top.getPath().length());
return new File(sbase, child);
}
}
return new File(_resourceBase, pathInContext); return new File(_resourceBase, pathInContext);
} }
@ -191,9 +209,18 @@ public class I2PSnarkServlet extends BasicServlet {
return; return;
} }
// in-war icons etc.
if (path != null && path.startsWith(WARBASE)) {
if (method.equals("GET") || method.equals("HEAD"))
super.doGet(req, resp);
else // no POST either
resp.sendError(405);
}
boolean isConfigure = "/configure".equals(path); boolean isConfigure = "/configure".equals(path);
// index.jsp doesn't work, it is grabbed by the war handler before here // 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("/")) { if (path.endsWith("/")) {
// Listing of a torrent (torrent detail page) // Listing of a torrent (torrent detail page)
// bypass the horrid Resource.getListHTML() // bypass the horrid Resource.getListHTML()
@ -848,42 +875,36 @@ public class I2PSnarkServlet extends BasicServlet {
_manager.addMessage(_("Data file could not be deleted: {0}", f.getAbsolutePath())); _manager.addMessage(_("Data file could not be deleted: {0}", f.getAbsolutePath()));
break; break;
} }
Storage storage = snark.getStorage();
if (storage == null)
break;
// step 1 delete files // step 1 delete files
for (int i = 0; i < files.size(); i++) { for (File df : storage.getFiles()) {
// 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));
if (df.delete()) { if (df.delete()) {
//_manager.addMessage(_("Data file deleted: {0}", df.getAbsolutePath())); //_manager.addMessage(_("Data file deleted: {0}", df.getAbsolutePath()));
} else { } else {
_manager.addMessage(_("Data file could not be deleted: {0}", df.getAbsolutePath())); _manager.addMessage(_("Data file could not be deleted: {0}", df.getAbsolutePath()));
} }
} }
// step 2 make Set of dirs with reverse sort // step 2 delete dirs bottom-up
Set<File> dirs = new TreeSet<File>(Collections.reverseOrder()); Set<File> dirs = storage.getDirectories();
for (List<String> list : files) { if (_log.shouldLog(Log.INFO))
for (int i = 1; i < list.size(); i++) { _log.info("Dirs to delete: " + DataHelper.toString(dirs));
dirs.add(Storage.getFileFromNames(f, list.subList(0, i))); boolean ok = false;
}
}
// step 3 delete dirs bottom-up
for (File df : dirs) { for (File df : dirs) {
if (df.delete()) { if (df.delete()) {
ok = true;
//_manager.addMessage(_("Data dir deleted: {0}", df.getAbsolutePath())); //_manager.addMessage(_("Data dir deleted: {0}", df.getAbsolutePath()));
} else { } else {
ok = false;
_manager.addMessage(_("Directory could not be deleted: {0}", df.getAbsolutePath())); _manager.addMessage(_("Directory could not be deleted: {0}", df.getAbsolutePath()));
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("Could not delete dir " + df); _log.warn("Could not delete dir " + df);
} }
} }
// step 4 delete base // step 3 message for base (last one)
if (f.delete()) { if (ok)
_manager.addMessage(_("Directory deleted: {0}", f.getAbsolutePath())); _manager.addMessage(_("Directory deleted: {0}", storage.getBase()));
} else {
_manager.addMessage(_("Directory could not be deleted: {0}", f.getAbsolutePath()));
if (_log.shouldLog(Log.WARN))
_log.warn("Could not delete dir " + f);
}
break; break;
} }
} }
@ -922,7 +943,9 @@ public class I2PSnarkServlet extends BasicServlet {
} else if ("Create".equals(action)) { } else if ("Create".equals(action)) {
String baseData = req.getParameter("baseFile"); String baseData = req.getParameter("baseFile");
if (baseData != null && baseData.trim().length() > 0) { 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"); String announceURL = req.getParameter("announceURL");
// make the user add a tracker on the config form now // make the user add a tracker on the config form now
//String announceURLOther = req.getParameter("announceURLOther"); //String announceURLOther = req.getParameter("announceURLOther");
@ -982,7 +1005,7 @@ public class I2PSnarkServlet extends BasicServlet {
File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent"); File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
// FIXME is the storage going to stay around thanks to the info reference? // FIXME is the storage going to stay around thanks to the info reference?
// now add it, but don't automatically start it // 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()); _manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
if (announceURL != null && !_manager.util().getOpenTrackers().contains(announceURL)) 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())); _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
@ -1783,10 +1806,11 @@ public class I2PSnarkServlet extends BasicServlet {
out.write("</span><hr>\n<table border=\"0\"><tr><td>"); out.write("</span><hr>\n<table border=\"0\"><tr><td>");
//out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br>\n"); //out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br>\n");
out.write(_("Data to seed")); out.write(_("Data to seed"));
out.write(":<td><code>" + _manager.getDataDir().getAbsolutePath() + File.separatorChar out.write(":<td>"
+ "</code><input type=\"text\" name=\"baseFile\" size=\"58\" value=\"" + baseFile + "<input type=\"text\" name=\"baseFile\" size=\"58\" value=\"" + baseFile
+ "\" spellcheck=\"false\" title=\""); + "\" spellcheck=\"false\" title=\"");
out.write(_("File or directory to seed (must be within the specified path)")); out.write(_("File or directory to seed (full path or within the directory {0} )",
_manager.getDataDir().getAbsolutePath() + File.separatorChar));
out.write("\" ><tr><td>\n"); out.write("\" ><tr><td>\n");
out.write(_("Trackers")); out.write(_("Trackers"));
out.write(":<td><table style=\"width: 30%;\"><tr><td></td><td align=\"center\">"); out.write(":<td><table style=\"width: 30%;\"><tr><td></td><td align=\"center\">");
@ -2230,9 +2254,11 @@ public class I2PSnarkServlet extends BasicServlet {
private static class ListingComparator implements Comparator<File>, Serializable { private static class ListingComparator implements Comparator<File>, Serializable {
public int compare(File l, File r) { 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; return -1;
if (r.isDirectory() && !l.isDirectory()) if (rd && !ld)
return 1; return 1;
return Collator.getInstance().compare(l.getName(), r.getName()); return Collator.getInstance().compare(l.getName(), r.getName());
} }
@ -2261,22 +2287,16 @@ public class I2PSnarkServlet extends BasicServlet {
* </pre> * </pre>
* *
* Get the resource list as a HTML directory listing. * Get the resource list as a HTML directory listing.
* @param r The Resource * @param xxxr The Resource unused
* @param base The base URL * @param base The base URL
* @param parent True if the parent directory should be included * @param parent True if the parent directory should be included
* @param postParams map of POST parameters or null if not a POST * @param postParams map of POST parameters or null if not a POST
* @return String of HTML or null if postParams != null * @return String of HTML or null if postParams != null
* @since 0.7.14 * @since 0.7.14
*/ */
private String getListHTML(File r, String base, boolean parent, Map<String, String[]> postParams) private String getListHTML(File xxxr, String base, boolean parent, Map<String, String[]> postParams)
throws IOException 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 title = decodePath(base);
String cpath = _contextPath + '/'; String cpath = _contextPath + '/';
if (title.startsWith(cpath)) if (title.startsWith(cpath))
@ -2284,19 +2304,47 @@ public class I2PSnarkServlet extends BasicServlet {
// Get the snark associated with this directory // Get the snark associated with this directory
String torrentName; String torrentName;
String pathInTorrent;
int slash = title.indexOf('/'); int slash = title.indexOf('/');
if (slash > 0) if (slash > 0) {
torrentName = title.substring(0, slash); torrentName = title.substring(0, slash);
else pathInTorrent = title.substring(slash);
} else {
torrentName = title; torrentName = title;
pathInTorrent = "/";
}
Snark snark = _manager.getTorrentByBaseName(torrentName); Snark snark = _manager.getTorrentByBaseName(torrentName);
if (snark != null && postParams != null) { if (snark != null && postParams != null) {
// caller must P-R-G // caller must P-R-G
String[] val = postParams.get("nonce");
if (val != null) {
String nonce = val[0];
if (String.valueOf(_nonce).equals(nonce))
savePriorities(snark, postParams); savePriorities(snark, postParams);
else
_manager.addMessage("Please retry form submission (bad nonce)");
}
return null; return null;
} }
File r;
if (snark != null) {
Storage storage = snark.getStorage();
if (storage != null) {
File sbase = storage.getBase();
if (pathInTorrent.equals("/"))
r = sbase;
else
r = new File(sbase, pathInTorrent);
} else {
// magnet, dummy
r = new File("");
}
} else {
// dummy
r = new File("");
}
StringBuilder buf=new StringBuilder(4096); StringBuilder buf=new StringBuilder(4096);
buf.append(DOCTYPE).append("<HTML><HEAD><TITLE>"); buf.append(DOCTYPE).append("<HTML><HEAD><TITLE>");
if (title.endsWith("/")) if (title.endsWith("/"))
@ -2306,7 +2354,7 @@ public class I2PSnarkServlet extends BasicServlet {
buf.append(title); buf.append(title);
buf.append("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" + buf.append("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" +
"</HEAD><BODY>\n<center><div class=\"snarknavbar\"><a href=\"").append(_contextPath).append("/\" title=\"Torrents\""); "</HEAD><BODY>\n<center><div class=\"snarknavbar\"><a href=\"").append(_contextPath).append("/\" title=\"Torrents\"");
buf.append(" class=\"snarkRefresh\"><img alt=\"\" border=\"0\" src=\"" + _imgPath + "arrow_refresh.png\">&nbsp;&nbsp;"); buf.append(" class=\"snarkRefresh\"><img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("arrow_refresh.png\">&nbsp;&nbsp;");
if (_contextName.equals(DEFAULT_NAME)) if (_contextName.equals(DEFAULT_NAME))
buf.append(_("I2PSnark")); buf.append(_("I2PSnark"));
else else
@ -2315,9 +2363,12 @@ public class I2PSnarkServlet extends BasicServlet {
if (parent) // always true if (parent) // always true
buf.append("<div class=\"page\"><div class=\"mainsection\">"); buf.append("<div class=\"page\"><div class=\"mainsection\">");
boolean showPriority = ls != null && snark != null && snark.getStorage() != null && !snark.getStorage().complete(); boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() &&
if (showPriority) r.isDirectory();
if (showPriority) {
buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n"); buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
buf.append("<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\" >\n");
}
if (snark != null) { if (snark != null) {
// first table - torrent info // first table - torrent info
buf.append("<table class=\"snarkTorrentInfo\">\n"); buf.append("<table class=\"snarkTorrentInfo\">\n");
@ -2330,11 +2381,17 @@ public class I2PSnarkServlet extends BasicServlet {
String fullPath = snark.getName(); String fullPath = snark.getName();
String baseName = urlEncode((new File(fullPath)).getName()); String baseName = urlEncode((new File(fullPath)).getName());
buf.append("<tr><td>") buf.append("<tr><td>")
.append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "file.png\" >&nbsp;<b>") .append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
.append(_("Torrent file")) .append(_("Torrent file"))
.append(":</b> <a href=\"").append(_contextPath).append('/').append(baseName).append("\">") .append(":</b> <a href=\"").append(_contextPath).append('/').append(baseName).append("\">")
.append(fullPath) .append(fullPath)
.append("</a></td></tr>\n"); .append("</a></td></tr>\n");
buf.append("<tr><td>")
.append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
.append(_("Data location"))
.append(":</b> ")
.append(urlEncode(snark.getStorage().getBase().getPath()))
.append("</td></tr>\n");
String announce = null; String announce = null;
MetaInfo meta = snark.getMetaInfo(); MetaInfo meta = snark.getMetaInfo();
@ -2435,40 +2492,40 @@ public class I2PSnarkServlet extends BasicServlet {
// .append(MAGGOT).append(hex).append(':').append(hex).append("</a></td></tr>"); // .append(MAGGOT).append(hex).append(':').append(hex).append("</a></td></tr>");
buf.append("<tr><td>") buf.append("<tr><td>")
.append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "size.png\" >&nbsp;<b>") .append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("size.png\" >&nbsp;<b>")
.append(_("Size")) .append(_("Size"))
.append(":</b> ") .append(":</b> ")
.append(formatSize(snark.getTotalLength())); .append(formatSize(snark.getTotalLength()));
int pieces = snark.getPieces(); int pieces = snark.getPieces();
double completion = (pieces - snark.getNeeded()) / (double) pieces; double completion = (pieces - snark.getNeeded()) / (double) pieces;
if (completion < 1.0) if (completion < 1.0)
buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"" + _imgPath + "head_rx.png\" >&nbsp;<b>") buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("head_rx.png\" >&nbsp;<b>")
.append(_("Completion")) .append(_("Completion"))
.append(":</b> ") .append(":</b> ")
.append((new DecimalFormat("0.00%")).format(completion)); .append((new DecimalFormat("0.00%")).format(completion));
else else
buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"" + _imgPath + "head_rx.png\" >&nbsp;") buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("head_rx.png\" >&nbsp;")
.append(_("Complete")); .append(_("Complete"));
// else unknown // else unknown
long needed = snark.getNeededLength(); long needed = snark.getNeededLength();
if (needed > 0) if (needed > 0)
buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"" + _imgPath + "head_rx.png\" >&nbsp;<b>") buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("head_rx.png\" >&nbsp;<b>")
.append(_("Remaining")) .append(_("Remaining"))
.append(":</b> ") .append(":</b> ")
.append(formatSize(needed)); .append(formatSize(needed));
if (meta != null) { if (meta != null) {
List<List<String>> files = meta.getFiles(); List<List<String>> files = meta.getFiles();
int fileCount = files != null ? files.size() : 1; int fileCount = files != null ? files.size() : 1;
buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"" + _imgPath + "file.png\" >&nbsp;<b>") buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
.append(_("Files")) .append(_("Files"))
.append(":</b> ") .append(":</b> ")
.append(fileCount); .append(fileCount);
} }
buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"" + _imgPath + "file.png\" >&nbsp;<b>") buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
.append(_("Pieces")) .append(_("Pieces"))
.append(":</b> ") .append(":</b> ")
.append(pieces); .append(pieces);
buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"" + _imgPath + "file.png\" >&nbsp;<b>") buf.append("&nbsp;<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
.append(_("Piece size")) .append(_("Piece size"))
.append(":</b> ") .append(":</b> ")
.append(formatSize(snark.getPieceLength(0))) .append(formatSize(snark.getPieceLength(0)))
@ -2481,6 +2538,22 @@ public class I2PSnarkServlet extends BasicServlet {
.append("\"</th></tr>\n"); .append("\"</th></tr>\n");
} }
buf.append("</table>\n"); buf.append("</table>\n");
if (snark != null && !r.exists()) {
// fixup TODO
buf.append("<p>Does not exist<br>resource=\"").append(r.toString())
.append("\"<br>base=\"").append(base)
.append("\"<br>torrent=\"").append(torrentName)
.append("\"</p></div></div></BODY></HTML>");
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) { if (ls == null) {
// We are only showing the torrent info section // We are only showing the torrent info section
buf.append("</div></div></BODY></HTML>"); buf.append("</div></div></BODY></HTML>");
@ -2491,7 +2564,7 @@ public class I2PSnarkServlet extends BasicServlet {
buf.append("<table class=\"snarkDirInfo\"><thead>\n"); buf.append("<table class=\"snarkDirInfo\"><thead>\n");
buf.append("<tr>\n") buf.append("<tr>\n")
.append("<th colspan=2>") .append("<th colspan=2>")
.append("<img border=\"0\" src=\"" + _imgPath + "file.png\" title=\"") .append("<img border=\"0\" src=\"").append(_imgPath).append("file.png\" title=\"")
.append(_("Directory")) .append(_("Directory"))
.append(": ") .append(": ")
.append(directory) .append(directory)
@ -2499,20 +2572,20 @@ public class I2PSnarkServlet extends BasicServlet {
.append(_("Directory")) .append(_("Directory"))
.append("\"></th>\n"); .append("\"></th>\n");
buf.append("<th align=\"right\">") buf.append("<th align=\"right\">")
.append("<img border=\"0\" src=\"" + _imgPath + "size.png\" title=\"") .append("<img border=\"0\" src=\"").append(_imgPath).append("size.png\" title=\"")
.append(_("Size")) .append(_("Size"))
.append("\" alt=\"") .append("\" alt=\"")
.append(_("Size")) .append(_("Size"))
.append("\"></th>\n"); .append("\"></th>\n");
buf.append("<th class=\"headerstatus\">") buf.append("<th class=\"headerstatus\">")
.append("<img border=\"0\" src=\"" + _imgPath + "status.png\" title=\"") .append("<img border=\"0\" src=\"").append(_imgPath).append("status.png\" title=\"")
.append(_("Status")) .append(_("Status"))
.append("\" alt=\"") .append("\" alt=\"")
.append(_("Status")) .append(_("Status"))
.append("\"></th>\n"); .append("\"></th>\n");
if (showPriority) if (showPriority)
buf.append("<th class=\"headerpriority\">") buf.append("<th class=\"headerpriority\">")
.append("<img border=\"0\" src=\"" + _imgPath + "priority.png\" title=\"") .append("<img border=\"0\" src=\"").append(_imgPath).append("priority.png\" title=\"")
.append(_("Priority")) .append(_("Priority"))
.append("\" alt=\"") .append("\" alt=\"")
.append(_("Priority")) .append(_("Priority"))
@ -2520,7 +2593,7 @@ public class I2PSnarkServlet extends BasicServlet {
buf.append("</tr>\n</thead>\n"); buf.append("</tr>\n</thead>\n");
buf.append("<tr><td colspan=\"" + (showPriority ? '5' : '4') + "\" class=\"ParentDir\"><A HREF=\""); buf.append("<tr><td colspan=\"" + (showPriority ? '5' : '4') + "\" class=\"ParentDir\"><A HREF=\"");
buf.append(addPaths(base,"../")); buf.append(addPaths(base,"../"));
buf.append("\"><img alt=\"\" border=\"0\" src=\"" + _imgPath + "up.png\"> ") buf.append("\"><img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("up.png\"> ")
.append(_("Up to higher level directory")) .append(_("Up to higher level directory"))
.append("</A></td></tr>\n"); .append("</A></td></tr>\n");
@ -2544,6 +2617,7 @@ public class I2PSnarkServlet extends BasicServlet {
boolean complete = false; boolean complete = false;
String status = ""; String status = "";
long length = item.length(); long length = item.length();
int priority = 0;
if (item.isDirectory()) { if (item.isDirectory()) {
complete = true; complete = true;
//status = toImg("tick") + ' ' + _("Directory"); //status = toImg("tick") + ' ' + _("Directory");
@ -2554,9 +2628,8 @@ public class I2PSnarkServlet extends BasicServlet {
status = toImg("cancel") + ' ' + _("Torrent not found?"); status = toImg("cancel") + ' ' + _("Torrent not found?");
} else { } else {
Storage storage = snark.getStorage(); Storage storage = snark.getStorage();
try {
File f = item; long remaining = storage.remaining(item);
long remaining = storage.remaining(f.getCanonicalPath());
if (remaining < 0) { if (remaining < 0) {
complete = true; complete = true;
status = toImg("cancel") + ' ' + _("File not found in torrent?"); status = toImg("cancel") + ' ' + _("File not found in torrent?");
@ -2564,7 +2637,7 @@ public class I2PSnarkServlet extends BasicServlet {
complete = true; complete = true;
status = toImg("tick") + ' ' + _("Complete"); status = toImg("tick") + ' ' + _("Complete");
} else { } else {
int priority = storage.getPriority(f.getCanonicalPath()); priority = storage.getPriority(item);
if (priority < 0) if (priority < 0)
status = toImg("cancel"); status = toImg("cancel");
else if (priority == 0) else if (priority == 0)
@ -2575,9 +2648,7 @@ public class I2PSnarkServlet extends BasicServlet {
(100 * (length - remaining) / length) + "% " + _("complete") + (100 * (length - remaining) / length) + "% " + _("complete") +
" (" + DataHelper.formatSize2(remaining) + "B " + _("remaining") + ")"; " (" + DataHelper.formatSize2(remaining) + "B " + _("remaining") + ")";
} }
} catch (IOException ioe) {
status = "Not a file? " + ioe;
}
} }
} }
@ -2617,21 +2688,19 @@ public class I2PSnarkServlet extends BasicServlet {
buf.append("</TD>"); buf.append("</TD>");
if (showPriority) { if (showPriority) {
buf.append("<td class=\"priority\">"); buf.append("<td class=\"priority\">");
File f = item;
if ((!complete) && (!item.isDirectory())) { if ((!complete) && (!item.isDirectory())) {
int pri = snark.getStorage().getPriority(f.getCanonicalPath()); buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(item).append("\" ");
buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(f.getCanonicalPath()).append("\" "); if (priority > 0)
if (pri > 0)
buf.append("checked=\"true\""); buf.append("checked=\"true\"");
buf.append('>').append(_("High")); buf.append('>').append(_("High"));
buf.append("<input type=\"radio\" value=\"0\" name=\"pri.").append(f.getCanonicalPath()).append("\" "); buf.append("<input type=\"radio\" value=\"0\" name=\"pri.").append(item).append("\" ");
if (pri == 0) if (priority == 0)
buf.append("checked=\"true\""); buf.append("checked=\"true\"");
buf.append('>').append(_("Normal")); buf.append('>').append(_("Normal"));
buf.append("<input type=\"radio\" value=\"-9\" name=\"pri.").append(f.getCanonicalPath()).append("\" "); buf.append("<input type=\"radio\" value=\"-9\" name=\"pri.").append(item).append("\" ");
if (pri < 0) if (priority < 0)
buf.append("checked=\"true\""); buf.append("checked=\"true\"");
buf.append('>').append(_("Skip")); buf.append('>').append(_("Skip"));
showSaveButton = true; showSaveButton = true;
@ -2703,6 +2772,8 @@ public class I2PSnarkServlet extends BasicServlet {
icon = "application"; icon = "application";
else if (plc.endsWith(".iso")) else if (plc.endsWith(".iso"))
icon = "cd"; icon = "cd";
else if (mime.equals("application/x-bittorrent"))
icon = "magnet";
else else
icon = "page_white"; icon = "page_white";
return icon; return icon;
@ -2727,7 +2798,7 @@ public class I2PSnarkServlet extends BasicServlet {
String key = entry.getKey(); String key = entry.getKey();
if (key.startsWith("pri.")) { if (key.startsWith("pri.")) {
try { try {
String file = key.substring(4); File file = new File(key.substring(4));
String val = entry.getValue()[0]; // jetty arrays String val = entry.getValue()[0]; // jetty arrays
int pri = Integer.parseInt(val); int pri = Integer.parseInt(val);
storage.setPriority(file, pri); storage.setPriority(file, pri);
@ -2736,6 +2807,6 @@ public class I2PSnarkServlet extends BasicServlet {
} }
} }
snark.updatePiecePriorities(); snark.updatePiecePriorities();
_manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities()); _manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities(), storage.getBase());
} }
} }

View File

@ -24,6 +24,7 @@ su2 = application/zip
su3 = application/zip su3 = application/zip
sud = application/zip sud = application/zip
tbz = application/x-bzip2 tbz = application/x-bzip2
torrent = application/x-bittorrent
txt = text/plain txt = text/plain
war = application/java-archive war = application/java-archive
webm = video/webm webm = video/webm