From 47712a39aca0606f34eca9762cf12c5624a43885 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Mon, 27 Jan 2014 13:41:38 +0000 Subject: [PATCH] i2psnark: - Support arbitrary location for torrent data. Save location in per-torrent config file. TODO: Fix torrent browse pages (ticket #1028) - Enhance idle shutdown message - Javadocs --- .../java/src/org/klomp/snark/IdleChecker.java | 3 +- .../java/src/org/klomp/snark/PeerState.java | 2 +- .../java/src/org/klomp/snark/Snark.java | 49 ++++++++++++-- .../src/org/klomp/snark/SnarkManager.java | 64 ++++++++++++++----- .../java/src/org/klomp/snark/Storage.java | 21 +++--- .../org/klomp/snark/web/I2PSnarkServlet.java | 46 +++++++++---- 6 files changed, 138 insertions(+), 47 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java b/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java index ff6454ee23..58a73b668f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java +++ b/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java @@ -66,7 +66,8 @@ class IdleChecker extends SimpleTimer2.TimedEvent { if (_log.shouldLog(Log.WARN)) _log.warn("Closing tunnels on idle"); _util.disconnect(); - _mgr.addMessage(_util.getString("I2P tunnel closed.")); + _mgr.addMessage(_util.getString("No more torrents running.") + ' ' + + _util.getString("I2P tunnel closed.")); schedule(3 * CHECK_TIME); return; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java index 35008394e8..cf295a530d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java @@ -163,7 +163,7 @@ class PeerState implements DataLoader _log.debug(peer + " rcv bitfield"); if (bitfield != null) { - // XXX - Be liberal in what you except? + // XXX - Be liberal in what you accept? if (_log.shouldLog(Log.WARN)) _log.warn("Got unexpected bitfield message from " + peer); return; diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 398d9bd6ea..20d8f4294d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -34,6 +34,7 @@ import net.i2p.I2PAppContext; import net.i2p.client.streaming.I2PServerSocket; import net.i2p.data.Destination; import net.i2p.util.Log; +import net.i2p.util.SecureFile; /** * Main Snark program startup class. @@ -238,13 +239,21 @@ public class Snark private volatile boolean _autoStoppable; - /** 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; @@ -395,7 +421,14 @@ public class Snark try { activity = "Checking storage"; - storage = new Storage(_util, rootDataDir, 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(completeListener.getSavedTorrentTime(this), completeListener.getSavedTorrentBitField(this)); @@ -1102,8 +1135,14 @@ 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, rootDataDir, metainfo, this); + storage = new Storage(_util, baseFile, metainfo, this); storage.check(); // ... so don't set meta until here meta = metainfo; diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index dc1a9e92d6..92a14e400a 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -86,6 +86,7 @@ public class SnarkManager implements CompleteListener { public static final String PROP_DIR = "i2psnark.dir"; 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"; @@ -347,7 +348,7 @@ public class SnarkManager implements CompleteListener { * * @return the new config directory, non-null * @throws RuntimeException on creation fail - * @since 0.9.10 + * @since 0.9.11 */ private File migrateConfig(File oldFile) { File dir = new SecureDirectory(oldFile + CONFIG_DIR_SUFFIX); @@ -455,7 +456,7 @@ public class SnarkManager implements CompleteListener { /** * The config for a torrent * @return non-null, possibly empty - * @since 0.9.10 + * @since 0.9.11 */ private Properties getConfig(Snark snark) { return getConfig(snark.getInfoHash()); @@ -465,7 +466,7 @@ public class SnarkManager implements CompleteListener { * The config for a torrent * @param ih 20-byte infohash * @return non-null, possibly empty - * @since 0.9.10 + * @since 0.9.11 */ private Properties getConfig(byte[] ih) { Properties rv = new OrderedProperties(); @@ -482,7 +483,7 @@ public class SnarkManager implements CompleteListener { * The config file for a torrent * @param confDir the config directory * @param ih 20-byte infohash - * @since 0.9.10 + * @since 0.9.11 */ private static File configFile(File confDir, byte[] ih) { String hex = I2PSnarkUtil.toHex(ih); @@ -1071,15 +1072,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(); @@ -1160,9 +1169,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); @@ -1305,14 +1318,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()); @@ -1321,11 +1337,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); @@ -1461,6 +1477,19 @@ 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 @@ -1471,14 +1500,15 @@ public class SnarkManager implements CompleteListener { * * @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); + locked_saveTorrentStatus(metainfo, bitfield, priorities, base); } } - private void locked_saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) { + private void locked_saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, File base) { byte[] ih = metainfo.getInfoHash(); String bfs; if (bitfield.complete()) { @@ -1490,6 +1520,8 @@ public class SnarkManager implements CompleteListener { 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 if (priorities != null) { @@ -1742,7 +1774,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()); } /** @@ -1764,7 +1796,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 { @@ -1865,7 +1897,7 @@ public class SnarkManager implements CompleteListener { try { // Snark.fatal() throws a RuntimeException // don't let one bad torrent kill the whole loop - addTorrent(name, !shouldAutoStart()); + addTorrent(name, null, !shouldAutoStart()); } catch (Exception e) { addMessage(_("Error: Could not add the torrent {0}", name) + ": " + e); _log.error("Unable to add the torrent " + name, e); diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java index e9bf050b8c..b353f4284f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java @@ -86,20 +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, File rootDir, MetaInfo metainfo, StorageListener listener) + public Storage(I2PSnarkUtil util, File baseFile, MetaInfo metainfo, StorageListener listener) { _util = util; _log = util.getContext().logManager().getLog(Storage.class); - boolean areFilesPublic = _util.getFilesPublic(); - if (areFilesPublic) - _base = new File(rootDir, filterName(metainfo.getName())); - else - _base = new SecureFile(rootDir, filterName(metainfo.getName())); + _base = baseFile; this.metainfo = metainfo; this.listener = listener; needed = metainfo.getPieces(); @@ -708,7 +706,8 @@ public class Storage /** * The base file or directory. - * @return a new List + * @return the File + * @since 0.9.11 */ public File getBase() { return _base; @@ -716,8 +715,8 @@ public class Storage /** * Does not include directories. Unsorted. - * @since 0.9.10 * @return a new List + * @since 0.9.11 */ public List<File> getFiles() { List<File> rv = new ArrayList<File>(_torrentFiles.size()); @@ -731,7 +730,7 @@ public class Storage * Includes the base for a multi-file torrent. * Sorted bottom-up for easy deletion. * Slow. Use for deletion only. - * @since 0.9.10 + * @since 0.9.11 * @return a new Set or null for a single-file torrent */ public SortedSet<File> getDirectories() { diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index be980112ab..82c0345d7c 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -896,7 +896,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"); @@ -956,7 +958,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())); @@ -1708,10 +1710,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\">"); @@ -2198,12 +2201,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)) @@ -2249,7 +2246,8 @@ 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(); + 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"); @@ -2271,6 +2269,12 @@ public class I2PSnarkServlet extends BasicServlet { .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\" > <b>") + .append(_("Data location")) + .append(":</b> ") + .append(urlEncode(snark.getStorage().getBase().getPath())) + .append("</td></tr>\n"); MetaInfo meta = snark.getMetaInfo(); if (meta != null) { @@ -2404,6 +2408,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>"); @@ -2655,6 +2675,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()); } } -- GitLab