propagate from branch 'i2p.i2p' (head 1f9b91f318a0f2369243844a3cf7f485528492d7)

to branch 'i2p.i2p.zzz.snarkconfig' (head 37b27b6d354d62487294fd9276504b98a23f1057)
This commit is contained in:
zzz
2014-06-21 13:02:22 +00:00
8 changed files with 514 additions and 219 deletions

View File

@ -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;
}

View File

@ -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;

View File

@ -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) {

View File

@ -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<String> _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<String>();
_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<String, Tracker>(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<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 */
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();
@ -1300,23 +1482,38 @@ 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);

View File

@ -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<TorrentFile> _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<Long> ls = metainfo.getLengths();
int size = files.size();
@ -546,7 +527,7 @@ public class Storage
for (int i = 0; i < size; 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
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<String> names, boolean areFilesPublic) throws IOException
{
@ -725,15 +708,47 @@ public class Storage
return f;
}
public static File getFileFromNames(File base, List<String> names)
{
Iterator<String> 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<File> getFiles() {
List<File> rv = new ArrayList<File>(_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<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.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();

View File

@ -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<File> dirs = new TreeSet<File>(Collections.reverseOrder());
for (List<String> 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<File> 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("</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(_("Data to seed"));
out.write(":<td><code>" + _manager.getDataDir().getAbsolutePath() + File.separatorChar
+ "</code><input type=\"text\" name=\"baseFile\" size=\"58\" value=\"" + baseFile
out.write(":<td>"
+ "<input type=\"text\" name=\"baseFile\" size=\"58\" value=\"" + baseFile
+ "\" 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(_("Trackers"));
out.write(":<td><table style=\"width: 30%;\"><tr><td></td><td align=\"center\">");
@ -2221,9 +2218,11 @@ public class I2PSnarkServlet extends BasicServlet {
private static class ListingComparator implements Comparator<File>, 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<String, String[]> 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("</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\"");
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))
buf.append(_("I2PSnark"));
else
@ -2306,9 +2306,12 @@ public class I2PSnarkServlet extends BasicServlet {
if (parent) // always true
buf.append("<div class=\"page\"><div class=\"mainsection\">");
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("<form action=\"").append(base).append("\" method=\"POST\">\n");
buf.append("<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\" >\n");
}
if (snark != null) {
// first table - torrent info
buf.append("<table class=\"snarkTorrentInfo\">\n");
@ -2321,11 +2324,17 @@ public class I2PSnarkServlet extends BasicServlet {
String fullPath = snark.getName();
String baseName = urlEncode((new File(fullPath)).getName());
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(":</b> <a href=\"").append(_contextPath).append('/').append(baseName).append("\">")
.append(fullPath)
.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;
MetaInfo meta = snark.getMetaInfo();
@ -2426,40 +2435,40 @@ public class I2PSnarkServlet extends BasicServlet {
// .append(MAGGOT).append(hex).append(':').append(hex).append("</a></td></tr>");
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(":</b> ")
.append(formatSize(snark.getTotalLength()));
int pieces = snark.getPieces();
double completion = (pieces - snark.getNeeded()) / (double) pieces;
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(":</b> ")
.append((new DecimalFormat("0.00%")).format(completion));
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"));
// else unknown
long needed = snark.getNeededLength();
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(":</b> ")
.append(formatSize(needed));
if (meta != null) {
List<List<String>> files = meta.getFiles();
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(":</b> ")
.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(":</b> ")
.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(":</b> ")
.append(formatSize(snark.getPieceLength(0)))
@ -2472,6 +2481,22 @@ public class I2PSnarkServlet extends BasicServlet {
.append("\"</th></tr>\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) {
// We are only showing the torrent info section
buf.append("</div></div></BODY></HTML>");
@ -2482,7 +2507,7 @@ public class I2PSnarkServlet extends BasicServlet {
buf.append("<table class=\"snarkDirInfo\"><thead>\n");
buf.append("<tr>\n")
.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(": ")
.append(directory)
@ -2490,20 +2515,20 @@ public class I2PSnarkServlet extends BasicServlet {
.append(_("Directory"))
.append("\"></th>\n");
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("\" alt=\"")
.append(_("Size"))
.append("\"></th>\n");
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("\" alt=\"")
.append(_("Status"))
.append("\"></th>\n");
if (showPriority)
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("\" alt=\"")
.append(_("Priority"))
@ -2511,7 +2536,7 @@ public class I2PSnarkServlet extends BasicServlet {
buf.append("</tr>\n</thead>\n");
buf.append("<tr><td colspan=\"" + (showPriority ? '5' : '4') + "\" class=\"ParentDir\"><A HREF=\"");
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("</A></td></tr>\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("</TD>");
if (showPriority) {
buf.append("<td class=\"priority\">");
File f = item;
if ((!complete) && (!item.isDirectory())) {
int pri = snark.getStorage().getPriority(f.getCanonicalPath());
buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
if (pri > 0)
buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(item).append("\" ");
if (priority > 0)
buf.append("checked=\"true\"");
buf.append('>').append(_("High"));
buf.append("<input type=\"radio\" value=\"0\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
if (pri == 0)
buf.append("<input type=\"radio\" value=\"0\" name=\"pri.").append(item).append("\" ");
if (priority == 0)
buf.append("checked=\"true\"");
buf.append('>').append(_("Normal"));
buf.append("<input type=\"radio\" value=\"-9\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
if (pri < 0)
buf.append("<input type=\"radio\" value=\"-9\" name=\"pri.").append(item).append("\" ");
if (priority < 0)
buf.append("checked=\"true\"");
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());
}
}

View File

@ -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