- Move some torrent file creation code from the servlet to the manager,

to allow locking and prevent interference by the DirMonitor
- More checks for whether torrent is already running
- Consistent filename filtering in all cases
- Allow null announce string
- Move snarks map to a CHM
- Remember last tracker selection
- Add callback for reception of metainfo
This commit is contained in:
zzz
2010-12-21 16:36:08 +00:00
parent 8e40b35210
commit f15b329874
5 changed files with 255 additions and 52 deletions

View File

@ -64,6 +64,8 @@ public class MetaInfo
/**
* Called by Storage when creating a new torrent from local data
*
* @param announce may be null
*/
MetaInfo(String announce, String name, String name_utf8, List files, List lengths,
int piece_length, byte[] piece_hashes, long length)
@ -86,6 +88,7 @@ public class MetaInfo
* Creates a new MetaInfo from the given InputStream. The
* InputStream must start with a correctly bencoded dictonary
* describing the torrent.
* Caller must close the stream.
*/
public MetaInfo(InputStream in) throws IOException
{
@ -107,7 +110,9 @@ public class MetaInfo
* the original bencoded info dictonary (this is a hack, we could
* reconstruct the bencoded stream and recalculate the hash). Will
* NOT throw a InvalidBEncodingException if the given map does not
* contain a valid announce string or info dictonary.
* contain a valid announce string.
* WILL throw a InvalidBEncodingException if the given map does not
* contain a valid info dictionary.
*/
public MetaInfo(Map m) throws InvalidBEncodingException
{
@ -401,7 +406,8 @@ public class MetaInfo
public synchronized byte[] getTorrentData()
{
Map m = new HashMap();
m.put("announce", announce);
if (announce != null)
m.put("announce", announce);
Map info = createInfoMap();
m.put("info", info);
// don't save this locally, we should only do this once

View File

@ -1084,6 +1084,16 @@ public class Snark
public interface CompleteListener {
public void torrentComplete(Snark snark);
public void updateStatus(Snark snark);
/**
* We transitioned from magnet mode, we have now initialized our
* metainfo and storage. The listener should now call getMetaInfo()
* and save the data to disk.
*
* @since 0.8.4
*/
public void gotMetaInfo(Snark snark);
// not really listeners but the easiest way to get back to an optional SnarkManager
public long getSavedTorrentTime(Snark snark);
public BitField getSavedTorrentBitField(Snark snark);

View File

@ -5,6 +5,7 @@ import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@ -16,15 +17,18 @@ import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.I2PAppContext;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
import net.i2p.util.OrderedProperties;
import net.i2p.util.SecureDirectory;
import net.i2p.util.SecureFileOutputStream;
/**
* Manage multiple snarks
@ -33,7 +37,11 @@ public class SnarkManager implements Snark.CompleteListener {
private static SnarkManager _instance = new SnarkManager();
public static SnarkManager instance() { return _instance; }
/** map of (canonical) filename of the .torrent file to Snark instance (unsynchronized) */
/**
* Map of (canonical) filename of the .torrent file to Snark instance.
* This is a CHM so listTorrentFiles() need not be synced, but
* all adds, deletes, and the DirMonitor should sync on it.
*/
private final Map<String, Snark> _snarks;
/** used to prevent DirMonitor from deleting torrents that don't have a torrent file yet */
private final Set<String> _magnets;
@ -74,7 +82,7 @@ public class SnarkManager implements Snark.CompleteListener {
public static final int DEFAULT_MAX_UP_BW = 10;
public static final int DEFAULT_STARTUP_DELAY = 3;
private SnarkManager() {
_snarks = new HashMap();
_snarks = new ConcurrentHashMap();
_magnets = new ConcurrentHashSet();
_addSnarkLock = new Object();
_context = I2PAppContext.getGlobalContext();
@ -462,8 +470,13 @@ public class SnarkManager implements Snark.CompleteListener {
/** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */
private static final int MAX_FILES_PER_TORRENT = 512;
/** set of canonical .torrent filenames that we are dealing with */
public Set<String> listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } }
/**
* Set of canonical .torrent filenames that we are dealing with.
* An unsynchronized copy.
*/
public Set<String> listTorrentFiles() {
return new HashSet(_snarks.keySet());
}
/**
* Grab the torrent given the (canonical) filename of the .torrent file
@ -486,10 +499,31 @@ public class SnarkManager implements Snark.CompleteListener {
return null;
}
/** @throws RuntimeException via Snark.fatal() */
/**
* Grab the torrent given the info hash
* @return Snark or null
* @since 0.8.4
*/
public Snark getTorrentByInfoHash(byte[] infohash) {
synchronized (_snarks) {
for (Snark s : _snarks.values()) {
if (DataHelper.eq(infohash, s.getInfoHash()))
return s;
}
}
return null;
}
/**
* Caller must verify this torrent is not already added.
* @throws RuntimeException via Snark.fatal()
*/
public void addTorrent(String filename) { addTorrent(filename, false); }
/** @throws RuntimeException via Snark.fatal() */
/**
* Caller must verify this torrent is not already added.
* @throws RuntimeException via Snark.fatal()
*/
public void addTorrent(String filename, boolean dontAutoStart) {
if ((!dontAutoStart) && !_util.connected()) {
addMessage(_("Connecting to I2P"));
@ -545,13 +579,14 @@ public class SnarkManager implements Snark.CompleteListener {
dontAutoStart = true;
}
}
String rejectMessage = locked_validateTorrent(info);
String rejectMessage = validateTorrent(info);
if (rejectMessage != null) {
sfile.delete();
addMessage(rejectMessage);
return;
} else {
// TODO load saved closest DHT nodes and pass to the Snark ?
// This may take a LONG time
torrent = new Snark(_util, filename, null, -1, null, null, this,
_peerCoordinatorSet, _connectionAcceptor,
false, dataDir.getPath());
@ -595,11 +630,10 @@ public class SnarkManager implements Snark.CompleteListener {
false, getDataDir().getPath());
synchronized (_snarks) {
for (Snark snark : _snarks.values()) {
if (DataHelper.eq(ih, snark.getInfoHash())) {
addMessage(_("Torrent already running: {0}", snark.getBaseName()));
return;
}
Snark snark = getTorrentByInfoHash(ih);
if (snark != null) {
addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName()));
return;
}
// Tell the dir monitor not to delete us
_magnets.add(name);
@ -633,7 +667,99 @@ public class SnarkManager implements Snark.CompleteListener {
}
/**
* Get the timestamp for a torrent from the config file
* Add a torrent from a MetaInfo. Save the MetaInfo data to filename.
* Holds the snarks lock to prevent interference from the DirMonitor.
* This verifies that a torrent with this infohash is not already added.
* This may take a LONG time to create or check the storage.
*
* @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.
* @throws RuntimeException via Snark.fatal()
* @since 0.8.4
*/
public void addTorrent(MetaInfo metainfo, BitField bitfield, String filename, boolean dontAutoStart) throws IOException {
// prevent interference by DirMonitor
synchronized (_snarks) {
Snark snark = getTorrentByInfoHash(metainfo.getInfoHash());
if (snark != null) {
addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName()));
return;
}
// so addTorrent won't recheck
saveTorrentStatus(metainfo, bitfield, null); // no file priorities
try {
locked_writeMetaInfo(metainfo, filename);
// hold the lock for a long time
addTorrent(filename, dontAutoStart);
} catch (IOException ioe) {
addMessage(_("Failed to copy torrent file to {0}", filename));
_log.error("Failed to write torrent file", ioe);
}
}
}
/**
* Add a torrent from a file not in the torrent directory. Copy the file to filename.
* Holds the snarks lock to prevent interference from the DirMonitor.
* Caller must verify this torrent is not already added.
* This may take a LONG time to create or check the storage.
*
* @param fromfile where the file is now, presumably in a temp directory somewhere
* @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.
* @throws RuntimeException via Snark.fatal()
* @since 0.8.4
*/
public void copyAndAddTorrent(File fromfile, String filename) throws IOException {
// prevent interference by DirMonitor
synchronized (_snarks) {
boolean success = FileUtil.copy(fromfile.getAbsolutePath(), filename, false);
if (!success) {
addMessage(_("Failed to copy torrent file to {0}", filename));
_log.error("Failed to write torrent file to " + filename);
return;
}
SecureFileOutputStream.setPerms(new File(filename));
// hold the lock for a long time
addTorrent(filename);
}
}
/**
* Write the metainfo to the file, caller must hold the snarks lock
* to prevent interference from the DirMonitor.
*
* @param metainfo The metainfo for the torrent
* @param filename The absolute path to save the metainfo to, generally ending in ".torrent".
* Must be a filesystem-safe name.
* @since 0.8.4
*/
private void locked_writeMetaInfo(MetaInfo metainfo, String filename) throws IOException {
// prevent interference by DirMonitor
File file = new File(filename);
if (file.exists())
throw new IOException("Cannot overwrite an existing .torrent file: " + file.getPath());
OutputStream out = null;
try {
out = new SecureFileOutputStream(filename);
out.write(metainfo.getTorrentData());
} catch (IOException ioe) {
// remove any partial
file.delete();
throw ioe;
} finally {
try {
if (out == null)
out.close();
} catch (IOException ioe) {}
}
}
/**
* Get the timestamp for a torrent from the config file.
* A Snark.CompleteListener method.
*/
public long getSavedTorrentTime(Snark snark) {
byte[] ih = snark.getInfoHash();
@ -653,6 +779,7 @@ public class SnarkManager implements Snark.CompleteListener {
/**
* Get the saved bitfield for a torrent from the config file.
* Convert "." to a full bitfield.
* A Snark.CompleteListener method.
*/
public BitField getSavedTorrentBitField(Snark snark) {
MetaInfo metainfo = snark.getMetaInfo();
@ -721,6 +848,8 @@ public class SnarkManager implements Snark.CompleteListener {
* 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
*/
public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
@ -783,9 +912,11 @@ public class SnarkManager implements Snark.CompleteListener {
}
/**
* Does not really delete on failure, that's the caller's responsibility.
* Warning - does not validate announce URL - use TrackerClient.isValidAnnounce()
* @return failure message or null on success
*/
private String locked_validateTorrent(MetaInfo info) throws IOException {
private String validateTorrent(MetaInfo info) {
List files = info.getFiles();
if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size());
@ -866,17 +997,22 @@ public class SnarkManager implements Snark.CompleteListener {
/**
* Stop the torrent and delete the torrent file itself, but leaving the data
* behind.
* Holds the snarks lock to prevent interference from the DirMonitor.
*/
public void removeTorrent(String filename) {
Snark torrent = stopTorrent(filename, true);
if (torrent != null) {
Snark torrent;
// prevent interference by DirMonitor
synchronized (_snarks) {
torrent = stopTorrent(filename, true);
if (torrent == null)
return;
File torrentFile = new File(filename);
torrentFile.delete();
Storage storage = torrent.getStorage();
if (storage != null)
removeTorrentStatus(storage.getMetaInfo());
addMessage(_("Torrent removed: \"{0}\"", torrent.getBaseName()));
}
Storage storage = torrent.getStorage();
if (storage != null)
removeTorrentStatus(storage.getMetaInfo());
addMessage(_("Torrent removed: \"{0}\"", torrent.getBaseName()));
}
private class DirMonitor implements Runnable {
@ -901,7 +1037,10 @@ public class SnarkManager implements Snark.CompleteListener {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Directory Monitor loop over " + dir.getAbsolutePath());
try {
monitorTorrents(dir);
// Don't let this interfere with .torrent files being added or deleted
synchronized (_snarks) {
monitorTorrents(dir);
}
} catch (Exception e) {
_log.error("Error in the DirectoryMonitor", e);
}
@ -910,7 +1049,11 @@ public class SnarkManager implements Snark.CompleteListener {
}
}
/** two listeners */
// Begin Snark.CompleteListeners
/**
* A Snark.CompleteListener method.
*/
public void torrentComplete(Snark snark) {
MetaInfo meta = snark.getMetaInfo();
Storage storage = snark.getStorage();
@ -925,6 +1068,9 @@ public class SnarkManager implements Snark.CompleteListener {
updateStatus(snark);
}
/**
* A Snark.CompleteListener method.
*/
public void updateStatus(Snark snark) {
MetaInfo meta = snark.getMetaInfo();
Storage storage = snark.getStorage();
@ -932,6 +1078,39 @@ public class SnarkManager implements Snark.CompleteListener {
saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities());
}
/**
* We transitioned from magnet mode, we have now initialized our
* metainfo and storage. The listener should now call getMetaInfo()
* and save the data to disk.
* A Snark.CompleteListener method.
*
* @since 0.8.4
*/
public void gotMetaInfo(Snark snark) {
MetaInfo meta = snark.getMetaInfo();
Storage storage = snark.getStorage();
if (meta != null && storage != null) {
String rejectMessage = validateTorrent(meta);
if (rejectMessage != null) {
addMessage(rejectMessage);
snark.stopTorrent();
return;
}
saveTorrentStatus(meta, storage.getBitField(), null); // no file priorities
String name = (new File(getDataDir(), storage.getBaseName() + ".torrent")).getAbsolutePath();
try {
synchronized (_snarks) {
locked_writeMetaInfo(meta, name);
}
} catch (IOException ioe) {
addMessage(_("Failed to copy torrent file to {0}", name));
_log.error("Failed to write torrent file", ioe);
}
}
}
// End Snark.CompleteListeners
private void monitorTorrents(File dir) {
String fileNames[] = dir.list(TorrentFilenameFilter.instance());
List<String> foundNames = new ArrayList(0);

View File

@ -87,6 +87,9 @@ public class Storage
* Creates a storage from the existing file or directory together
* with an appropriate MetaInfo file as can be announced on the
* given announce String location.
*
* @param announce may be null
* @param listener may be null
*/
public Storage(I2PSnarkUtil util, File baseFile, String announce, StorageListener listener)
throws IOException
@ -590,7 +593,7 @@ public class Storage
* Removes 'suspicious' characters from the given file name.
* http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx
*/
private static String filterName(String name)
public static String filterName(String name)
{
if (name.equals(".") || name.equals(" "))
return "_";

View File

@ -59,6 +59,7 @@ public class I2PSnarkServlet extends Default {
private Resource _resourceBase;
private String _themePath;
private String _imgPath;
private String _lastAnnounceURL = "";
public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
/** BEP 9 */
@ -605,23 +606,23 @@ public class I2PSnarkServlet extends Default {
if (announceURL == null || announceURL.length() <= 0)
_manager.addMessage(_("Error creating torrent - you must select a tracker"));
else if (baseFile.exists()) {
_lastAnnounceURL = announceURL;
if (announceURL.equals("none"))
announceURL = null;
try {
// This may take a long time to check the storage, but since it already exists,
// it shouldn't be THAT bad, so keep it in this thread.
Storage s = new Storage(_manager.util(), baseFile, announceURL, null);
s.create();
s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
MetaInfo info = s.getMetaInfo();
File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent");
if (torrentFile.exists())
throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath());
_manager.saveTorrentStatus(info, s.getBitField(), null); // so addTorrent won't recheck
// DirMonitor could grab this first, maybe hold _snarks lock?
FileOutputStream out = new FileOutputStream(torrentFile);
out.write(info.getTorrentData());
out.close();
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.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
// now fire it up, but don't automatically seed it
_manager.addTorrent(torrentFile.getCanonicalPath(), true);
_manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
if (announceURL != null)
_manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
} catch (IOException ioe) {
_manager.addMessage(_("Error creating a torrent for \"{0}\"", baseFile.getAbsolutePath()) + ": " + ioe.getMessage());
}
@ -1165,6 +1166,10 @@ public class I2PSnarkServlet extends Default {
out.write(":<td><select name=\"announceURL\"><option value=\"\">");
out.write(_("Select a tracker"));
out.write("</option>\n");
// todo remember this one with _lastAnnounceURL also
out.write("<option value=\"none\">");
out.write(_("Open trackers and DHT only"));
out.write("</option>\n");
Map trackers = _manager.getTrackers();
for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry entry = (Map.Entry)iter.next();
@ -1173,6 +1178,8 @@ public class I2PSnarkServlet extends Default {
int e = announceURL.indexOf('=');
if (e > 0)
announceURL = announceURL.substring(0, e);
if (announceURL.equals(_lastAnnounceURL))
announceURL += "\" selected=\"selected";
out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n");
}
out.write("</select>\n");
@ -1801,15 +1808,18 @@ private static class FetchAndAdd implements Runnable {
FileInputStream in = null;
try {
in = new FileInputStream(file);
// we do not retain this MetaInfo object, hopefully it will go away quickly
MetaInfo info = new MetaInfo(in);
String name = info.getName();
name = DataHelper.stripHTML(name); // XSS
name = name.replace('/', '_');
name = name.replace('\\', '_');
name = name.replace('&', '+');
name = name.replace('\'', '_');
name = name.replace('"', '_');
name = name.replace('`', '_');
try { in.close(); } catch (IOException ioe) {}
Snark snark = _manager.getTorrentByInfoHash(info.getInfoHash());
if (snark != null) {
_manager.addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName()));
return;
}
// don't hold object from this MetaInfo
String name = new String(info.getName());
name = Storage.filterName(name);
name = name + ".torrent";
File torrentFile = new File(_manager.getDataDir(), name);
@ -1821,18 +1831,13 @@ private static class FetchAndAdd implements Runnable {
else
_manager.addMessage(_("Torrent already in the queue: {0}", name));
} else {
boolean success = FileUtil.copy(file.getAbsolutePath(), canonical, false);
if (success) {
SecureFileOutputStream.setPerms(torrentFile);
_manager.addTorrent(canonical);
} else {
_manager.addMessage(_("Failed to copy torrent file to {0}", canonical));
}
// This may take a LONG time to create the storage.
_manager.copyAndAddTorrent(file, canonical);
}
} catch (IOException ioe) {
_manager.addMessage(_("Torrent at {0} was not valid", urlify(_url)) + ": " + ioe.getMessage());
} finally {
try { in.close(); } catch (IOException ioe) {}
try { if (in != null) in.close(); } catch (IOException ioe) {}
}
} else {
_manager.addMessage(_("Torrent was not retrieved from {0}", urlify(_url)));