propagate from branch 'i2p.i2p.zzz.test4' (head 8dcdde2b4d8eec6e6832866f70995392b52aa23d)
to branch 'i2p.i2p.zzz.dhtsnark' (head fb155ffe63e76eaa33cd79c9604a555b11a27989)
This commit is contained in:
BIN
apps/i2psnark/_icons/magnet.png
Normal file
BIN
apps/i2psnark/_icons/magnet.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 591 B |
@ -137,6 +137,11 @@ public class ConnectionAcceptor implements Runnable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (socket.getPeerDestination().equals(_util.getMyDestination())) {
|
||||||
|
_util.debug("Incoming connection from myself", Snark.ERROR);
|
||||||
|
try { socket.close(); } catch (IOException ioe) {}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Thread t = new I2PAppThread(new Handler(socket), "I2PSnark incoming connection");
|
Thread t = new I2PAppThread(new Handler(socket), "I2PSnark incoming connection");
|
||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,12 @@ public interface CoordinatorListener
|
|||||||
*/
|
*/
|
||||||
void peerChange(PeerCoordinator coordinator, Peer peer);
|
void peerChange(PeerCoordinator coordinator, Peer peer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the PeerCoordinator got the MetaInfo via magnet.
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo);
|
||||||
|
|
||||||
public boolean overUploadLimit(int uploaders);
|
public boolean overUploadLimit(int uploaders);
|
||||||
public boolean overUpBWLimit();
|
public boolean overUpBWLimit();
|
||||||
public boolean overUpBWLimit(long total);
|
public boolean overUpBWLimit(long total);
|
||||||
|
347
apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java
Normal file
347
apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
package org.klomp.snark;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import net.i2p.I2PAppContext;
|
||||||
|
import net.i2p.data.DataHelper;
|
||||||
|
import net.i2p.util.Log;
|
||||||
|
|
||||||
|
import org.klomp.snark.bencode.BDecoder;
|
||||||
|
import org.klomp.snark.bencode.BEncoder;
|
||||||
|
import org.klomp.snark.bencode.BEValue;
|
||||||
|
import org.klomp.snark.bencode.InvalidBEncodingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REF: BEP 10 Extension Protocol
|
||||||
|
* @since 0.8.2
|
||||||
|
* @author zzz
|
||||||
|
*/
|
||||||
|
abstract class ExtensionHandler {
|
||||||
|
|
||||||
|
private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(ExtensionHandler.class);
|
||||||
|
|
||||||
|
public static final int ID_HANDSHAKE = 0;
|
||||||
|
public static final int ID_METADATA = 1;
|
||||||
|
public static final String TYPE_METADATA = "ut_metadata";
|
||||||
|
public static final int ID_PEX = 2;
|
||||||
|
public static final String TYPE_PEX = "ut_pex";
|
||||||
|
/** Pieces * SHA1 Hash length, + 25% extra for file names, benconding overhead, etc */
|
||||||
|
private static final int MAX_METADATA_SIZE = Storage.MAX_PIECES * 20 * 5 / 4;
|
||||||
|
private static final int PARALLEL_REQUESTS = 3;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param metasize -1 if unknown
|
||||||
|
* @return bencoded outgoing handshake message
|
||||||
|
*/
|
||||||
|
public static byte[] getHandshake(int metasize) {
|
||||||
|
Map<String, Object> handshake = new HashMap();
|
||||||
|
Map<String, Integer> m = new HashMap();
|
||||||
|
m.put(TYPE_METADATA, Integer.valueOf(ID_METADATA));
|
||||||
|
m.put(TYPE_PEX, Integer.valueOf(ID_PEX));
|
||||||
|
if (metasize >= 0)
|
||||||
|
handshake.put("metadata_size", Integer.valueOf(metasize));
|
||||||
|
handshake.put("m", m);
|
||||||
|
handshake.put("p", Integer.valueOf(6881));
|
||||||
|
handshake.put("v", "I2PSnark");
|
||||||
|
handshake.put("reqq", Integer.valueOf(5));
|
||||||
|
return BEncoder.bencode(handshake);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handleMessage(Peer peer, PeerListener listener, int id, byte[] bs) {
|
||||||
|
if (_log.shouldLog(Log.INFO))
|
||||||
|
_log.info("Got extension msg " + id + " length " + bs.length + " from " + peer);
|
||||||
|
if (id == ID_HANDSHAKE)
|
||||||
|
handleHandshake(peer, listener, bs);
|
||||||
|
else if (id == ID_METADATA)
|
||||||
|
handleMetadata(peer, listener, bs);
|
||||||
|
else if (id == ID_PEX)
|
||||||
|
handlePEX(peer, listener, bs);
|
||||||
|
else if (_log.shouldLog(Log.INFO))
|
||||||
|
_log.info("Unknown extension msg " + id + " from " + peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleHandshake(Peer peer, PeerListener listener, byte[] bs) {
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Got handshake msg from " + peer);
|
||||||
|
try {
|
||||||
|
// this throws NPE on missing keys
|
||||||
|
InputStream is = new ByteArrayInputStream(bs);
|
||||||
|
BDecoder dec = new BDecoder(is);
|
||||||
|
BEValue bev = dec.bdecodeMap();
|
||||||
|
Map<String, BEValue> map = bev.getMap();
|
||||||
|
peer.setHandshakeMap(map);
|
||||||
|
Map<String, BEValue> msgmap = map.get("m").getMap();
|
||||||
|
|
||||||
|
if (msgmap.get(TYPE_PEX) != null) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.debug("Peer supports PEX extension: " + peer);
|
||||||
|
// peer state calls peer listener calls sendPEX()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgmap.get(TYPE_METADATA) == null) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.debug("Peer does not support metadata extension: " + peer);
|
||||||
|
// drop if we need metainfo ?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BEValue msize = map.get("metadata_size");
|
||||||
|
if (msize == null) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.debug("Peer does not have the metainfo size yet: " + peer);
|
||||||
|
// drop if we need metainfo ?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int metaSize = msize.getInt();
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.debug("Got the metainfo size: " + metaSize);
|
||||||
|
|
||||||
|
MagnetState state = peer.getMagnetState();
|
||||||
|
int remaining;
|
||||||
|
synchronized(state) {
|
||||||
|
if (state.isComplete())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (state.isInitialized()) {
|
||||||
|
if (state.getSize() != metaSize) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.debug("Wrong metainfo size " + metaSize + " from: " + peer);
|
||||||
|
peer.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// initialize it
|
||||||
|
if (metaSize > MAX_METADATA_SIZE) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.debug("Huge metainfo size " + metaSize + " from: " + peer);
|
||||||
|
peer.disconnect(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_log.shouldLog(Log.INFO))
|
||||||
|
_log.info("Initialized state, metadata size = " + metaSize + " from " + peer);
|
||||||
|
state.initialize(metaSize);
|
||||||
|
}
|
||||||
|
remaining = state.chunksRemaining();
|
||||||
|
}
|
||||||
|
|
||||||
|
// send requests for chunks
|
||||||
|
int count = Math.min(remaining, PARALLEL_REQUESTS);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int chk;
|
||||||
|
synchronized(state) {
|
||||||
|
chk = state.getNextRequest();
|
||||||
|
}
|
||||||
|
if (_log.shouldLog(Log.INFO))
|
||||||
|
_log.info("Request chunk " + chk + " from " + peer);
|
||||||
|
sendRequest(peer, chk);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.warn("Handshake exception from " + peer, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int TYPE_REQUEST = 0;
|
||||||
|
private static final int TYPE_DATA = 1;
|
||||||
|
private static final int TYPE_REJECT = 2;
|
||||||
|
|
||||||
|
private static final int CHUNK_SIZE = 16*1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REF: BEP 9
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
private static void handleMetadata(Peer peer, PeerListener listener, byte[] bs) {
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Got metadata msg from " + peer);
|
||||||
|
try {
|
||||||
|
InputStream is = new ByteArrayInputStream(bs);
|
||||||
|
BDecoder dec = new BDecoder(is);
|
||||||
|
BEValue bev = dec.bdecodeMap();
|
||||||
|
Map<String, BEValue> map = bev.getMap();
|
||||||
|
int type = map.get("msg_type").getInt();
|
||||||
|
int piece = map.get("piece").getInt();
|
||||||
|
|
||||||
|
MagnetState state = peer.getMagnetState();
|
||||||
|
if (type == TYPE_REQUEST) {
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Got request for " + piece + " from: " + peer);
|
||||||
|
byte[] pc;
|
||||||
|
synchronized(state) {
|
||||||
|
pc = state.getChunk(piece);
|
||||||
|
}
|
||||||
|
sendPiece(peer, piece, pc);
|
||||||
|
// Do this here because PeerConnectionOut only reports for PIECE messages
|
||||||
|
peer.uploaded(pc.length);
|
||||||
|
listener.uploaded(peer, pc.length);
|
||||||
|
} else if (type == TYPE_DATA) {
|
||||||
|
int size = map.get("total_size").getInt();
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Got data for " + piece + " length " + size + " from: " + peer);
|
||||||
|
boolean done;
|
||||||
|
int chk = -1;
|
||||||
|
synchronized(state) {
|
||||||
|
if (state.isComplete())
|
||||||
|
return;
|
||||||
|
int len = is.available();
|
||||||
|
if (len != size) {
|
||||||
|
// probably fatal
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.warn("total_size " + size + " but avail data " + len);
|
||||||
|
}
|
||||||
|
peer.downloaded(len);
|
||||||
|
listener.downloaded(peer, len);
|
||||||
|
done = state.saveChunk(piece, bs, bs.length - len, len);
|
||||||
|
if (_log.shouldLog(Log.INFO))
|
||||||
|
_log.info("Got chunk " + piece + " from " + peer);
|
||||||
|
if (!done)
|
||||||
|
chk = state.getNextRequest();
|
||||||
|
}
|
||||||
|
// out of the lock
|
||||||
|
if (done) {
|
||||||
|
// Done!
|
||||||
|
// PeerState will call the listener (peer coord), who will
|
||||||
|
// check to see if the MagnetState has it
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.warn("Got last chunk from " + peer);
|
||||||
|
} else {
|
||||||
|
// get the next chunk
|
||||||
|
if (_log.shouldLog(Log.INFO))
|
||||||
|
_log.info("Request chunk " + chk + " from " + peer);
|
||||||
|
sendRequest(peer, chk);
|
||||||
|
}
|
||||||
|
} else if (type == TYPE_REJECT) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.warn("Got reject msg from " + peer);
|
||||||
|
peer.disconnect(false);
|
||||||
|
} else {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.warn("Got unknown metadata msg from " + peer);
|
||||||
|
peer.disconnect(false);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.info("Metadata ext. msg. exception from " + peer, e);
|
||||||
|
// fatal ?
|
||||||
|
peer.disconnect(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendRequest(Peer peer, int piece) {
|
||||||
|
sendMessage(peer, TYPE_REQUEST, piece);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendReject(Peer peer, int piece) {
|
||||||
|
sendMessage(peer, TYPE_REJECT, piece);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** REQUEST and REJECT are the same except for message type */
|
||||||
|
private static void sendMessage(Peer peer, int type, int piece) {
|
||||||
|
Map<String, Object> map = new HashMap();
|
||||||
|
map.put("msg_type", Integer.valueOf(type));
|
||||||
|
map.put("piece", Integer.valueOf(piece));
|
||||||
|
byte[] payload = BEncoder.bencode(map);
|
||||||
|
try {
|
||||||
|
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_METADATA).getInt();
|
||||||
|
peer.sendExtension(hisMsgCode, payload);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// NPE, no metadata capability
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.info("Metadata send req msg exception to " + peer, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendPiece(Peer peer, int piece, byte[] data) {
|
||||||
|
Map<String, Object> map = new HashMap();
|
||||||
|
map.put("msg_type", Integer.valueOf(TYPE_DATA));
|
||||||
|
map.put("piece", Integer.valueOf(piece));
|
||||||
|
map.put("total_size", Integer.valueOf(data.length));
|
||||||
|
byte[] dict = BEncoder.bencode(map);
|
||||||
|
byte[] payload = new byte[dict.length + data.length];
|
||||||
|
System.arraycopy(dict, 0, payload, 0, dict.length);
|
||||||
|
System.arraycopy(data, 0, payload, dict.length, data.length);
|
||||||
|
try {
|
||||||
|
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_METADATA).getInt();
|
||||||
|
peer.sendExtension(hisMsgCode, payload);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// NPE, no metadata caps
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.info("Metadata send piece msg exception to " + peer, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int HASH_LENGTH = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can't find a published standard for this anywhere.
|
||||||
|
* See the libtorrent code.
|
||||||
|
* Here we use the "added" key as a single string of concatenated
|
||||||
|
* 32-byte peer hashes.
|
||||||
|
* added.f and dropped unsupported
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
private static void handlePEX(Peer peer, PeerListener listener, byte[] bs) {
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Got PEX msg from " + peer);
|
||||||
|
try {
|
||||||
|
InputStream is = new ByteArrayInputStream(bs);
|
||||||
|
BDecoder dec = new BDecoder(is);
|
||||||
|
BEValue bev = dec.bdecodeMap();
|
||||||
|
Map<String, BEValue> map = bev.getMap();
|
||||||
|
byte[] ids = map.get("added").getBytes();
|
||||||
|
if (ids.length < HASH_LENGTH)
|
||||||
|
return;
|
||||||
|
int len = Math.min(ids.length, (I2PSnarkUtil.MAX_CONNECTIONS - 1) * HASH_LENGTH);
|
||||||
|
List<PeerID> peers = new ArrayList(len / HASH_LENGTH);
|
||||||
|
for (int off = 0; off < len; off += HASH_LENGTH) {
|
||||||
|
byte[] hash = new byte[HASH_LENGTH];
|
||||||
|
System.arraycopy(ids, off, hash, 0, HASH_LENGTH);
|
||||||
|
if (DataHelper.eq(hash, peer.getPeerID().getDestHash()))
|
||||||
|
continue;
|
||||||
|
PeerID pID = new PeerID(hash);
|
||||||
|
peers.add(pID);
|
||||||
|
}
|
||||||
|
// could include ourselves, listener must remove
|
||||||
|
listener.gotPeers(peer, peers);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.info("PEX msg exception from " + peer, e);
|
||||||
|
//peer.disconnect(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* added.f and dropped unsupported
|
||||||
|
* @param pList non-null
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public static void sendPEX(Peer peer, List<Peer> pList) {
|
||||||
|
if (pList.isEmpty())
|
||||||
|
return;
|
||||||
|
Map<String, Object> map = new HashMap();
|
||||||
|
byte[] peers = new byte[HASH_LENGTH * pList.size()];
|
||||||
|
int off = 0;
|
||||||
|
for (Peer p : pList) {
|
||||||
|
System.arraycopy(p.getPeerID().getDestHash(), 0, peers, off, HASH_LENGTH);
|
||||||
|
off += HASH_LENGTH;
|
||||||
|
}
|
||||||
|
map.put("added", peers);
|
||||||
|
byte[] payload = BEncoder.bencode(map);
|
||||||
|
try {
|
||||||
|
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_PEX).getInt();
|
||||||
|
peer.sendExtension(hisMsgCode, payload);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// NPE, no PEX caps
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.info("PEX msg exception to " + peer, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
package org.klomp.snark;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.klomp.snark.bencode.BEncoder;
|
|
||||||
import org.klomp.snark.bencode.BEValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REF: BEP 10 Extension Protocol
|
|
||||||
* @since 0.8.2
|
|
||||||
*/
|
|
||||||
class ExtensionHandshake {
|
|
||||||
|
|
||||||
private static final byte[] _payload = buildPayload();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bencoded data
|
|
||||||
*/
|
|
||||||
static byte[] getPayload() {
|
|
||||||
return _payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** just a test for now */
|
|
||||||
private static byte[] buildPayload() {
|
|
||||||
Map<String, Object> handshake = new HashMap();
|
|
||||||
Map<String, Integer> m = new HashMap();
|
|
||||||
m.put("foo", Integer.valueOf(99));
|
|
||||||
m.put("bar", Integer.valueOf(101));
|
|
||||||
handshake.put("m", m);
|
|
||||||
handshake.put("p", Integer.valueOf(6881));
|
|
||||||
handshake.put("v", "I2PSnark");
|
|
||||||
handshake.put("reqq", Integer.valueOf(5));
|
|
||||||
return BEncoder.bencode(handshake);
|
|
||||||
}
|
|
||||||
}
|
|
@ -34,6 +34,9 @@ import net.i2p.util.SimpleScheduler;
|
|||||||
import net.i2p.util.SimpleTimer;
|
import net.i2p.util.SimpleTimer;
|
||||||
import net.i2p.util.Translate;
|
import net.i2p.util.Translate;
|
||||||
|
|
||||||
|
import org.klomp.snark.dht.DHT;
|
||||||
|
import org.klomp.snark.dht.KRPC;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I2P specific helpers for I2PSnark
|
* I2P specific helpers for I2PSnark
|
||||||
* We use this class as a sort of context for i2psnark
|
* We use this class as a sort of context for i2psnark
|
||||||
@ -58,6 +61,8 @@ public class I2PSnarkUtil {
|
|||||||
private int _maxConnections;
|
private int _maxConnections;
|
||||||
private File _tmpDir;
|
private File _tmpDir;
|
||||||
private int _startupDelay;
|
private int _startupDelay;
|
||||||
|
private boolean _shouldUseOT;
|
||||||
|
private DHT _dht;
|
||||||
|
|
||||||
public static final int DEFAULT_STARTUP_DELAY = 3;
|
public static final int DEFAULT_STARTUP_DELAY = 3;
|
||||||
public static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers";
|
public static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers";
|
||||||
@ -66,6 +71,8 @@ public class I2PSnarkUtil {
|
|||||||
public static final String DEFAULT_OPENTRACKERS = "http://tracker.welterde.i2p/a";
|
public static final String DEFAULT_OPENTRACKERS = "http://tracker.welterde.i2p/a";
|
||||||
public static final int DEFAULT_MAX_UP_BW = 8; //KBps
|
public static final int DEFAULT_MAX_UP_BW = 8; //KBps
|
||||||
public static final int MAX_CONNECTIONS = 16; // per torrent
|
public static final int MAX_CONNECTIONS = 16; // per torrent
|
||||||
|
private static final boolean ENABLE_DHT = true;
|
||||||
|
|
||||||
public I2PSnarkUtil(I2PAppContext ctx) {
|
public I2PSnarkUtil(I2PAppContext ctx) {
|
||||||
_context = ctx;
|
_context = ctx;
|
||||||
_log = _context.logManager().getLog(Snark.class);
|
_log = _context.logManager().getLog(Snark.class);
|
||||||
@ -78,6 +85,7 @@ public class I2PSnarkUtil {
|
|||||||
_maxUpBW = DEFAULT_MAX_UP_BW;
|
_maxUpBW = DEFAULT_MAX_UP_BW;
|
||||||
_maxConnections = MAX_CONNECTIONS;
|
_maxConnections = MAX_CONNECTIONS;
|
||||||
_startupDelay = DEFAULT_STARTUP_DELAY;
|
_startupDelay = DEFAULT_STARTUP_DELAY;
|
||||||
|
_shouldUseOT = DEFAULT_USE_OPENTRACKERS;
|
||||||
// This is used for both announce replies and .torrent file downloads,
|
// This is used for both announce replies and .torrent file downloads,
|
||||||
// so it must be available even if not connected to I2CP.
|
// so it must be available even if not connected to I2CP.
|
||||||
// so much for multiple instances
|
// so much for multiple instances
|
||||||
@ -187,10 +195,20 @@ public class I2PSnarkUtil {
|
|||||||
// opts.setProperty("i2p.streaming.readTimeout", "120000");
|
// opts.setProperty("i2p.streaming.readTimeout", "120000");
|
||||||
_manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts);
|
_manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts);
|
||||||
}
|
}
|
||||||
|
// FIXME this only instantiates krpc once, left stuck with old manager
|
||||||
|
if (ENABLE_DHT && _manager != null && _dht == null)
|
||||||
|
_dht = new KRPC(_context, _manager.getSession());
|
||||||
return (_manager != null);
|
return (_manager != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return null if disabled or not started
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public DHT getDHT() { return _dht; }
|
||||||
|
|
||||||
public boolean connected() { return _manager != null; }
|
public boolean connected() { return _manager != null; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy the destination itself
|
* Destroy the destination itself
|
||||||
*/
|
*/
|
||||||
@ -214,6 +232,8 @@ public class I2PSnarkUtil {
|
|||||||
Destination addr = peer.getAddress();
|
Destination addr = peer.getAddress();
|
||||||
if (addr == null)
|
if (addr == null)
|
||||||
throw new IOException("Null address");
|
throw new IOException("Null address");
|
||||||
|
if (addr.equals(getMyDestination()))
|
||||||
|
throw new IOException("Attempt to connect to myself");
|
||||||
Hash dest = addr.calculateHash();
|
Hash dest = addr.calculateHash();
|
||||||
if (_shitlist.contains(dest))
|
if (_shitlist.contains(dest))
|
||||||
throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are shitlisted");
|
throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are shitlisted");
|
||||||
@ -287,17 +307,25 @@ public class I2PSnarkUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getOurIPString() {
|
String getOurIPString() {
|
||||||
if (_manager == null)
|
Destination dest = getMyDestination();
|
||||||
return "unknown";
|
if (dest != null)
|
||||||
I2PSession sess = _manager.getSession();
|
return dest.toBase64();
|
||||||
if (sess != null) {
|
|
||||||
Destination dest = sess.getMyDestination();
|
|
||||||
if (dest != null)
|
|
||||||
return dest.toBase64();
|
|
||||||
}
|
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return dest or null
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
Destination getMyDestination() {
|
||||||
|
if (_manager == null)
|
||||||
|
return null;
|
||||||
|
I2PSession sess = _manager.getSession();
|
||||||
|
if (sess != null)
|
||||||
|
return sess.getMyDestination();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Base64 only - static (no naming service) */
|
/** Base64 only - static (no naming service) */
|
||||||
static Destination getDestinationFromBase64(String ip) {
|
static Destination getDestinationFromBase64(String ip) {
|
||||||
if (ip == null) return null;
|
if (ip == null) return null;
|
||||||
@ -400,10 +428,10 @@ public class I2PSnarkUtil {
|
|||||||
|
|
||||||
/** comma delimited list open trackers to use as backups */
|
/** comma delimited list open trackers to use as backups */
|
||||||
/** sorted map of name to announceURL=baseURL */
|
/** sorted map of name to announceURL=baseURL */
|
||||||
public List getOpenTrackers() {
|
public List<String> getOpenTrackers() {
|
||||||
if (!shouldUseOpenTrackers())
|
if (!shouldUseOpenTrackers())
|
||||||
return null;
|
return null;
|
||||||
List rv = new ArrayList(1);
|
List<String> rv = new ArrayList(1);
|
||||||
String trackers = getOpenTrackerString();
|
String trackers = getOpenTrackerString();
|
||||||
StringTokenizer tok = new StringTokenizer(trackers, ", ");
|
StringTokenizer tok = new StringTokenizer(trackers, ", ");
|
||||||
while (tok.hasMoreTokens())
|
while (tok.hasMoreTokens())
|
||||||
@ -414,11 +442,27 @@ public class I2PSnarkUtil {
|
|||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUseOpenTrackers(boolean yes) {
|
||||||
|
_shouldUseOT = yes;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean shouldUseOpenTrackers() {
|
public boolean shouldUseOpenTrackers() {
|
||||||
String rv = (String) _opts.get(PROP_USE_OPENTRACKERS);
|
return _shouldUseOT;
|
||||||
if (rv == null)
|
}
|
||||||
return DEFAULT_USE_OPENTRACKERS;
|
|
||||||
return Boolean.valueOf(rv).booleanValue();
|
/**
|
||||||
|
* Like DataHelper.toHexString but ensures no loss of leading zero bytes
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public static String toHex(byte[] b) {
|
||||||
|
StringBuilder buf = new StringBuilder(40);
|
||||||
|
for (int i = 0; i < b.length; i++) {
|
||||||
|
int bi = b[i] & 0xff;
|
||||||
|
if (bi < 16)
|
||||||
|
buf.append('0');
|
||||||
|
buf.append(Integer.toHexString(bi));
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** hook between snark's logger and an i2p log */
|
/** hook between snark's logger and an i2p log */
|
||||||
|
204
apps/i2psnark/java/src/org/klomp/snark/MagnetState.java
Normal file
204
apps/i2psnark/java/src/org/klomp/snark/MagnetState.java
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
package org.klomp.snark;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import net.i2p.I2PAppContext;
|
||||||
|
import net.i2p.data.DataHelper;
|
||||||
|
|
||||||
|
import org.klomp.snark.bencode.BDecoder;
|
||||||
|
import org.klomp.snark.bencode.BEValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple state for the download of the metainfo, shared between
|
||||||
|
* Peer and ExtensionHandler.
|
||||||
|
*
|
||||||
|
* Nothing is synchronized here!
|
||||||
|
* Caller must synchronize on this for everything!
|
||||||
|
*
|
||||||
|
* Reference: BEP 9
|
||||||
|
*
|
||||||
|
* @since 0.8.4
|
||||||
|
* author zzz
|
||||||
|
*/
|
||||||
|
class MagnetState {
|
||||||
|
public static final int CHUNK_SIZE = 16*1024;
|
||||||
|
private static final Random random = I2PAppContext.getGlobalContext().random();
|
||||||
|
|
||||||
|
private final byte[] infohash;
|
||||||
|
private boolean complete;
|
||||||
|
/** if false, nothing below is valid */
|
||||||
|
private boolean isInitialized;
|
||||||
|
|
||||||
|
private int metaSize;
|
||||||
|
private int totalChunks;
|
||||||
|
/** bitfield for the metainfo chunks - will remain null if we start out complete */
|
||||||
|
private BitField requested;
|
||||||
|
private BitField have;
|
||||||
|
/** bitfield for the metainfo */
|
||||||
|
private byte[] metainfoBytes;
|
||||||
|
/** only valid when finished */
|
||||||
|
private MetaInfo metainfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param meta null for new magnet
|
||||||
|
*/
|
||||||
|
public MagnetState(byte[] iHash, MetaInfo meta) {
|
||||||
|
infohash = iHash;
|
||||||
|
if (meta != null) {
|
||||||
|
metainfo = meta;
|
||||||
|
initialize(meta.getInfoBytes().length);
|
||||||
|
complete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param call this for a new magnet when you have the size
|
||||||
|
* @throws IllegalArgumentException
|
||||||
|
*/
|
||||||
|
public void initialize(int size) {
|
||||||
|
if (isInitialized)
|
||||||
|
throw new IllegalArgumentException("already set");
|
||||||
|
isInitialized = true;
|
||||||
|
metaSize = size;
|
||||||
|
totalChunks = (size + (CHUNK_SIZE - 1)) / CHUNK_SIZE;
|
||||||
|
if (metainfo != null) {
|
||||||
|
metainfoBytes = metainfo.getInfoBytes();
|
||||||
|
} else {
|
||||||
|
// we don't need these if complete
|
||||||
|
have = new BitField(totalChunks);
|
||||||
|
requested = new BitField(totalChunks);
|
||||||
|
metainfoBytes = new byte[metaSize];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Call this for a new magnet when the download is complete.
|
||||||
|
* @throws IllegalArgumentException
|
||||||
|
*/
|
||||||
|
public void setMetaInfo(MetaInfo meta) {
|
||||||
|
metainfo = meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IllegalArgumentException
|
||||||
|
*/
|
||||||
|
public MetaInfo getMetaInfo() {
|
||||||
|
if (!complete)
|
||||||
|
throw new IllegalArgumentException("not complete");
|
||||||
|
return metainfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IllegalArgumentException
|
||||||
|
*/
|
||||||
|
public int getSize() {
|
||||||
|
if (!isInitialized)
|
||||||
|
throw new IllegalArgumentException("not initialized");
|
||||||
|
return metaSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return isInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isComplete() {
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int chunkSize(int chunk) {
|
||||||
|
return Math.min(CHUNK_SIZE, metaSize - (chunk * CHUNK_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return chunk count */
|
||||||
|
public int chunksRemaining() {
|
||||||
|
if (!isInitialized)
|
||||||
|
throw new IllegalArgumentException("not initialized");
|
||||||
|
if (complete)
|
||||||
|
return 0;
|
||||||
|
return totalChunks - have.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return chunk number */
|
||||||
|
public int getNextRequest() {
|
||||||
|
if (!isInitialized)
|
||||||
|
throw new IllegalArgumentException("not initialized");
|
||||||
|
if (complete)
|
||||||
|
throw new IllegalArgumentException("complete");
|
||||||
|
int rand = random.nextInt(totalChunks);
|
||||||
|
for (int i = 0; i < totalChunks; i++) {
|
||||||
|
int chk = (i + rand) % totalChunks;
|
||||||
|
if (!(have.get(chk) || requested.get(chk))) {
|
||||||
|
requested.set(chk);
|
||||||
|
return chk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// all requested - end game
|
||||||
|
for (int i = 0; i < totalChunks; i++) {
|
||||||
|
int chk = (i + rand) % totalChunks;
|
||||||
|
if (!have.get(chk))
|
||||||
|
return chk;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IllegalArgumentException
|
||||||
|
*/
|
||||||
|
public byte[] getChunk(int chunk) {
|
||||||
|
if (!complete)
|
||||||
|
throw new IllegalArgumentException("not complete");
|
||||||
|
if (chunk < 0 || chunk >= totalChunks)
|
||||||
|
throw new IllegalArgumentException("bad chunk number");
|
||||||
|
int size = chunkSize(chunk);
|
||||||
|
byte[] rv = new byte[size];
|
||||||
|
System.arraycopy(metainfoBytes, chunk * CHUNK_SIZE, rv, 0, size);
|
||||||
|
// use meta.getInfoBytes() so we don't save it in memory
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if this was the last piece
|
||||||
|
* @throws NPE, IllegalArgumentException, IOException, ...
|
||||||
|
*/
|
||||||
|
public boolean saveChunk(int chunk, byte[] data, int off, int length) throws Exception {
|
||||||
|
if (!isInitialized)
|
||||||
|
throw new IllegalArgumentException("not initialized");
|
||||||
|
if (chunk < 0 || chunk >= totalChunks)
|
||||||
|
throw new IllegalArgumentException("bad chunk number");
|
||||||
|
if (have.get(chunk))
|
||||||
|
return false; // shouldn't happen if synced
|
||||||
|
int size = chunkSize(chunk);
|
||||||
|
if (size != length)
|
||||||
|
throw new IllegalArgumentException("bad chunk length");
|
||||||
|
System.arraycopy(data, off, metainfoBytes, chunk * CHUNK_SIZE, size);
|
||||||
|
have.set(chunk);
|
||||||
|
boolean done = have.complete();
|
||||||
|
if (done) {
|
||||||
|
metainfo = buildMetaInfo();
|
||||||
|
complete = true;
|
||||||
|
}
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if this was the last piece
|
||||||
|
* @throws NPE, IllegalArgumentException, IOException, ...
|
||||||
|
*/
|
||||||
|
public MetaInfo buildMetaInfo() throws Exception {
|
||||||
|
// top map has nothing in it but the info map (no announce)
|
||||||
|
Map<String, Object> map = new HashMap();
|
||||||
|
InputStream is = new ByteArrayInputStream(metainfoBytes);
|
||||||
|
BDecoder dec = new BDecoder(is);
|
||||||
|
BEValue bev = dec.bdecodeMap();
|
||||||
|
map.put("info", bev);
|
||||||
|
MetaInfo newmeta = new MetaInfo(map);
|
||||||
|
if (!DataHelper.eq(newmeta.getInfoHash(), infohash))
|
||||||
|
throw new IOException("info hash mismatch");
|
||||||
|
return newmeta;
|
||||||
|
}
|
||||||
|
}
|
@ -53,6 +53,7 @@ class Message
|
|||||||
|
|
||||||
// Used for HAVE, REQUEST, PIECE and CANCEL messages.
|
// Used for HAVE, REQUEST, PIECE and CANCEL messages.
|
||||||
// low byte used for EXTENSION message
|
// low byte used for EXTENSION message
|
||||||
|
// low two bytes used for PORT message
|
||||||
int piece;
|
int piece;
|
||||||
|
|
||||||
// Used for REQUEST, PIECE and CANCEL messages.
|
// Used for REQUEST, PIECE and CANCEL messages.
|
||||||
@ -103,10 +104,13 @@ class Message
|
|||||||
if (type == REQUEST || type == CANCEL)
|
if (type == REQUEST || type == CANCEL)
|
||||||
datalen += 4;
|
datalen += 4;
|
||||||
|
|
||||||
// length is 1 byte
|
// msg type is 1 byte
|
||||||
if (type == EXTENSION)
|
if (type == EXTENSION)
|
||||||
datalen += 1;
|
datalen += 1;
|
||||||
|
|
||||||
|
if (type == PORT)
|
||||||
|
datalen += 2;
|
||||||
|
|
||||||
// add length of data for piece or bitfield array.
|
// add length of data for piece or bitfield array.
|
||||||
if (type == BITFIELD || type == PIECE || type == EXTENSION)
|
if (type == BITFIELD || type == PIECE || type == EXTENSION)
|
||||||
datalen += len;
|
datalen += len;
|
||||||
@ -130,6 +134,9 @@ class Message
|
|||||||
if (type == EXTENSION)
|
if (type == EXTENSION)
|
||||||
dos.writeByte((byte) piece & 0xff);
|
dos.writeByte((byte) piece & 0xff);
|
||||||
|
|
||||||
|
if (type == PORT)
|
||||||
|
dos.writeShort(piece & 0xffff);
|
||||||
|
|
||||||
// Send actual data
|
// Send actual data
|
||||||
if (type == BITFIELD || type == PIECE || type == EXTENSION)
|
if (type == BITFIELD || type == PIECE || type == EXTENSION)
|
||||||
dos.write(data, off, len);
|
dos.write(data, off, len);
|
||||||
@ -160,6 +167,8 @@ class Message
|
|||||||
return "PIECE(" + piece + "," + begin + "," + length + ")";
|
return "PIECE(" + piece + "," + begin + "," + length + ")";
|
||||||
case CANCEL:
|
case CANCEL:
|
||||||
return "CANCEL(" + piece + "," + begin + "," + length + ")";
|
return "CANCEL(" + piece + "," + begin + "," + length + ")";
|
||||||
|
case PORT:
|
||||||
|
return "PORT(" + piece + ")";
|
||||||
case EXTENSION:
|
case EXTENSION:
|
||||||
return "EXTENSION(" + piece + ',' + data.length + ')';
|
return "EXTENSION(" + piece + ',' + data.length + ')';
|
||||||
default:
|
default:
|
||||||
|
@ -25,6 +25,7 @@ import java.io.InputStream;
|
|||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -53,37 +54,43 @@ public class MetaInfo
|
|||||||
private final byte[] info_hash;
|
private final byte[] info_hash;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String name_utf8;
|
private final String name_utf8;
|
||||||
private final List files;
|
private final List<List<String>> files;
|
||||||
private final List files_utf8;
|
private final List<List<String>> files_utf8;
|
||||||
private final List lengths;
|
private final List<Long> lengths;
|
||||||
private final int piece_length;
|
private final int piece_length;
|
||||||
private final byte[] piece_hashes;
|
private final byte[] piece_hashes;
|
||||||
private final long length;
|
private final long length;
|
||||||
private final Map infoMap;
|
private Map<String, BEValue> infoMap;
|
||||||
|
|
||||||
private byte[] torrentdata;
|
/**
|
||||||
|
* Called by Storage when creating a new torrent from local data
|
||||||
MetaInfo(String announce, String name, String name_utf8, List files, List lengths,
|
*
|
||||||
|
* @param announce may be null
|
||||||
|
* @param files null for single-file torrent
|
||||||
|
* @param lengths null for single-file torrent
|
||||||
|
*/
|
||||||
|
MetaInfo(String announce, String name, String name_utf8, List<List<String>> files, List<Long> lengths,
|
||||||
int piece_length, byte[] piece_hashes, long length)
|
int piece_length, byte[] piece_hashes, long length)
|
||||||
{
|
{
|
||||||
this.announce = announce;
|
this.announce = announce;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.name_utf8 = name_utf8;
|
this.name_utf8 = name_utf8;
|
||||||
this.files = files;
|
this.files = files == null ? null : Collections.unmodifiableList(files);
|
||||||
this.files_utf8 = null;
|
this.files_utf8 = null;
|
||||||
this.lengths = lengths;
|
this.lengths = lengths == null ? null : Collections.unmodifiableList(lengths);
|
||||||
this.piece_length = piece_length;
|
this.piece_length = piece_length;
|
||||||
this.piece_hashes = piece_hashes;
|
this.piece_hashes = piece_hashes;
|
||||||
this.length = length;
|
this.length = length;
|
||||||
|
|
||||||
this.info_hash = calculateInfoHash();
|
this.info_hash = calculateInfoHash();
|
||||||
infoMap = null;
|
//infoMap = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new MetaInfo from the given InputStream. The
|
* Creates a new MetaInfo from the given InputStream. The
|
||||||
* InputStream must start with a correctly bencoded dictonary
|
* InputStream must start with a correctly bencoded dictonary
|
||||||
* describing the torrent.
|
* describing the torrent.
|
||||||
|
* Caller must close the stream.
|
||||||
*/
|
*/
|
||||||
public MetaInfo(InputStream in) throws IOException
|
public MetaInfo(InputStream in) throws IOException
|
||||||
{
|
{
|
||||||
@ -104,23 +111,29 @@ public class MetaInfo
|
|||||||
* Creates a new MetaInfo from a Map of BEValues and the SHA1 over
|
* Creates a new MetaInfo from a Map of BEValues and the SHA1 over
|
||||||
* the original bencoded info dictonary (this is a hack, we could
|
* the original bencoded info dictonary (this is a hack, we could
|
||||||
* reconstruct the bencoded stream and recalculate the hash). Will
|
* reconstruct the bencoded stream and recalculate the hash). Will
|
||||||
* throw a InvalidBEncodingException if the given map does not
|
* 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
|
public MetaInfo(Map m) throws InvalidBEncodingException
|
||||||
{
|
{
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Creating a metaInfo: " + m, new Exception("source"));
|
_log.debug("Creating a metaInfo: " + m, new Exception("source"));
|
||||||
BEValue val = (BEValue)m.get("announce");
|
BEValue val = (BEValue)m.get("announce");
|
||||||
if (val == null)
|
// Disabled check, we can get info from a magnet now
|
||||||
throw new InvalidBEncodingException("Missing announce string");
|
if (val == null) {
|
||||||
this.announce = val.getString();
|
//throw new InvalidBEncodingException("Missing announce string");
|
||||||
|
this.announce = null;
|
||||||
|
} else {
|
||||||
|
this.announce = val.getString();
|
||||||
|
}
|
||||||
|
|
||||||
val = (BEValue)m.get("info");
|
val = (BEValue)m.get("info");
|
||||||
if (val == null)
|
if (val == null)
|
||||||
throw new InvalidBEncodingException("Missing info map");
|
throw new InvalidBEncodingException("Missing info map");
|
||||||
Map info = val.getMap();
|
Map info = val.getMap();
|
||||||
infoMap = info;
|
infoMap = Collections.unmodifiableMap(info);
|
||||||
|
|
||||||
val = (BEValue)info.get("name");
|
val = (BEValue)info.get("name");
|
||||||
if (val == null)
|
if (val == null)
|
||||||
@ -160,39 +173,39 @@ public class MetaInfo
|
|||||||
throw new InvalidBEncodingException
|
throw new InvalidBEncodingException
|
||||||
("Missing length number and/or files list");
|
("Missing length number and/or files list");
|
||||||
|
|
||||||
List list = val.getList();
|
List<BEValue> list = val.getList();
|
||||||
int size = list.size();
|
int size = list.size();
|
||||||
if (size == 0)
|
if (size == 0)
|
||||||
throw new InvalidBEncodingException("zero size files list");
|
throw new InvalidBEncodingException("zero size files list");
|
||||||
|
|
||||||
files = new ArrayList(size);
|
List<List<String>> m_files = new ArrayList(size);
|
||||||
files_utf8 = new ArrayList(size);
|
List<List<String>> m_files_utf8 = new ArrayList(size);
|
||||||
lengths = new ArrayList(size);
|
List<Long> m_lengths = new ArrayList(size);
|
||||||
long l = 0;
|
long l = 0;
|
||||||
for (int i = 0; i < list.size(); i++)
|
for (int i = 0; i < list.size(); i++)
|
||||||
{
|
{
|
||||||
Map desc = ((BEValue)list.get(i)).getMap();
|
Map<String, BEValue> desc = list.get(i).getMap();
|
||||||
val = (BEValue)desc.get("length");
|
val = desc.get("length");
|
||||||
if (val == null)
|
if (val == null)
|
||||||
throw new InvalidBEncodingException("Missing length number");
|
throw new InvalidBEncodingException("Missing length number");
|
||||||
long len = val.getLong();
|
long len = val.getLong();
|
||||||
lengths.add(new Long(len));
|
m_lengths.add(new Long(len));
|
||||||
l += len;
|
l += len;
|
||||||
|
|
||||||
val = (BEValue)desc.get("path");
|
val = (BEValue)desc.get("path");
|
||||||
if (val == null)
|
if (val == null)
|
||||||
throw new InvalidBEncodingException("Missing path list");
|
throw new InvalidBEncodingException("Missing path list");
|
||||||
List path_list = val.getList();
|
List<BEValue> path_list = val.getList();
|
||||||
int path_length = path_list.size();
|
int path_length = path_list.size();
|
||||||
if (path_length == 0)
|
if (path_length == 0)
|
||||||
throw new InvalidBEncodingException("zero size file path list");
|
throw new InvalidBEncodingException("zero size file path list");
|
||||||
|
|
||||||
List file = new ArrayList(path_length);
|
List<String> file = new ArrayList(path_length);
|
||||||
Iterator it = path_list.iterator();
|
Iterator<BEValue> it = path_list.iterator();
|
||||||
while (it.hasNext())
|
while (it.hasNext())
|
||||||
file.add(((BEValue)it.next()).getString());
|
file.add(it.next().getString());
|
||||||
|
|
||||||
files.add(file);
|
m_files.add(Collections.unmodifiableList(file));
|
||||||
|
|
||||||
val = (BEValue)desc.get("path.utf-8");
|
val = (BEValue)desc.get("path.utf-8");
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
@ -202,11 +215,14 @@ public class MetaInfo
|
|||||||
file = new ArrayList(path_length);
|
file = new ArrayList(path_length);
|
||||||
it = path_list.iterator();
|
it = path_list.iterator();
|
||||||
while (it.hasNext())
|
while (it.hasNext())
|
||||||
file.add(((BEValue)it.next()).getString());
|
file.add(it.next().getString());
|
||||||
files_utf8.add(file);
|
m_files_utf8.add(Collections.unmodifiableList(file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
files = Collections.unmodifiableList(m_files);
|
||||||
|
files_utf8 = Collections.unmodifiableList(m_files_utf8);
|
||||||
|
lengths = Collections.unmodifiableList(m_lengths);
|
||||||
length = l;
|
length = l;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,6 +231,7 @@ public class MetaInfo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the string representing the URL of the tracker for this torrent.
|
* Returns the string representing the URL of the tracker for this torrent.
|
||||||
|
* @return may be null!
|
||||||
*/
|
*/
|
||||||
public String getAnnounce()
|
public String getAnnounce()
|
||||||
{
|
{
|
||||||
@ -253,9 +270,8 @@ public class MetaInfo
|
|||||||
* a single name. It has the same size as the list returned by
|
* a single name. It has the same size as the list returned by
|
||||||
* getLengths().
|
* getLengths().
|
||||||
*/
|
*/
|
||||||
public List getFiles()
|
public List<List<String>> getFiles()
|
||||||
{
|
{
|
||||||
// XXX - Immutable?
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,9 +280,8 @@ public class MetaInfo
|
|||||||
* files, or null if it is a single file. It has the same size as
|
* files, or null if it is a single file. It has the same size as
|
||||||
* the list returned by getFiles().
|
* the list returned by getFiles().
|
||||||
*/
|
*/
|
||||||
public List getLengths()
|
public List<Long> getLengths()
|
||||||
{
|
{
|
||||||
// XXX - Immutable?
|
|
||||||
return lengths;
|
return lengths;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,26 +403,35 @@ public class MetaInfo
|
|||||||
piece_hashes, length);
|
piece_hashes, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getTorrentData()
|
/**
|
||||||
|
* Called by servlet to save a new torrent file generated from local data
|
||||||
|
*/
|
||||||
|
public synchronized byte[] getTorrentData()
|
||||||
{
|
{
|
||||||
if (torrentdata == null)
|
|
||||||
{
|
|
||||||
Map m = new HashMap();
|
Map m = new HashMap();
|
||||||
m.put("announce", announce);
|
if (announce != null)
|
||||||
|
m.put("announce", announce);
|
||||||
Map info = createInfoMap();
|
Map info = createInfoMap();
|
||||||
m.put("info", info);
|
m.put("info", info);
|
||||||
torrentdata = BEncoder.bencode(m);
|
// don't save this locally, we should only do this once
|
||||||
}
|
return BEncoder.bencode(m);
|
||||||
return torrentdata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map createInfoMap()
|
/** @since 0.8.4 */
|
||||||
|
public synchronized byte[] getInfoBytes() {
|
||||||
|
if (infoMap == null)
|
||||||
|
createInfoMap();
|
||||||
|
return BEncoder.bencode(infoMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return an unmodifiable view of the Map */
|
||||||
|
private Map<String, BEValue> createInfoMap()
|
||||||
{
|
{
|
||||||
|
// if we loaded this metainfo from a file, we have the map
|
||||||
|
if (infoMap != null)
|
||||||
|
return Collections.unmodifiableMap(infoMap);
|
||||||
|
// otherwise we must create it
|
||||||
Map info = new HashMap();
|
Map info = new HashMap();
|
||||||
if (infoMap != null) {
|
|
||||||
info.putAll(infoMap);
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
info.put("name", name);
|
info.put("name", name);
|
||||||
if (name_utf8 != null)
|
if (name_utf8 != null)
|
||||||
info.put("name.utf-8", name_utf8);
|
info.put("name.utf-8", name_utf8);
|
||||||
@ -429,7 +453,8 @@ public class MetaInfo
|
|||||||
}
|
}
|
||||||
info.put("files", l);
|
info.put("files", l);
|
||||||
}
|
}
|
||||||
return info;
|
infoMap = info;
|
||||||
|
return Collections.unmodifiableMap(infoMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] calculateInfoHash()
|
private byte[] calculateInfoHash()
|
||||||
|
@ -28,10 +28,15 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import net.i2p.client.streaming.I2PSocket;
|
import net.i2p.client.streaming.I2PSocket;
|
||||||
|
import net.i2p.data.DataHelper;
|
||||||
|
import net.i2p.data.Destination;
|
||||||
import net.i2p.util.Log;
|
import net.i2p.util.Log;
|
||||||
|
|
||||||
|
import org.klomp.snark.bencode.BEValue;
|
||||||
|
|
||||||
public class Peer implements Comparable
|
public class Peer implements Comparable
|
||||||
{
|
{
|
||||||
private Log _log = new Log(Peer.class);
|
private Log _log = new Log(Peer.class);
|
||||||
@ -39,17 +44,27 @@ public class Peer implements Comparable
|
|||||||
private final PeerID peerID;
|
private final PeerID peerID;
|
||||||
|
|
||||||
private final byte[] my_id;
|
private final byte[] my_id;
|
||||||
final MetaInfo metainfo;
|
private final byte[] infohash;
|
||||||
|
/** will start out null in magnet mode */
|
||||||
|
private MetaInfo metainfo;
|
||||||
|
private Map<String, BEValue> handshakeMap;
|
||||||
|
|
||||||
// The data in/output streams set during the handshake and used by
|
// The data in/output streams set during the handshake and used by
|
||||||
// the actual connections.
|
// the actual connections.
|
||||||
private DataInputStream din;
|
private DataInputStream din;
|
||||||
private DataOutputStream dout;
|
private DataOutputStream dout;
|
||||||
|
|
||||||
|
/** running counters */
|
||||||
|
private long downloaded;
|
||||||
|
private long uploaded;
|
||||||
|
|
||||||
// Keeps state for in/out connections. Non-null when the handshake
|
// Keeps state for in/out connections. Non-null when the handshake
|
||||||
// was successful, the connection setup and runs
|
// was successful, the connection setup and runs
|
||||||
PeerState state;
|
PeerState state;
|
||||||
|
|
||||||
|
/** shared across all peers on this torrent */
|
||||||
|
MagnetState magnetState;
|
||||||
|
|
||||||
private I2PSocket sock;
|
private I2PSocket sock;
|
||||||
|
|
||||||
private boolean deregister = true;
|
private boolean deregister = true;
|
||||||
@ -64,18 +79,20 @@ public class Peer implements Comparable
|
|||||||
static final long OPTION_EXTENSION = 0x0000000000100000l;
|
static final long OPTION_EXTENSION = 0x0000000000100000l;
|
||||||
static final long OPTION_FAST = 0x0000000000000004l;
|
static final long OPTION_FAST = 0x0000000000000004l;
|
||||||
static final long OPTION_DHT = 0x0000000000000001l;
|
static final long OPTION_DHT = 0x0000000000000001l;
|
||||||
|
static final long OPTION_AZMP = 0x1000000000000000l;
|
||||||
private long options;
|
private long options;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outgoing connection.
|
* Outgoing connection.
|
||||||
* Creates a disconnected peer given a PeerID, your own id and the
|
* Creates a disconnected peer given a PeerID, your own id and the
|
||||||
* relevant MetaInfo.
|
* relevant MetaInfo.
|
||||||
|
* @param metainfo null if in magnet mode
|
||||||
*/
|
*/
|
||||||
public Peer(PeerID peerID, byte[] my_id, MetaInfo metainfo)
|
public Peer(PeerID peerID, byte[] my_id, byte[] infohash, MetaInfo metainfo)
|
||||||
throws IOException
|
|
||||||
{
|
{
|
||||||
this.peerID = peerID;
|
this.peerID = peerID;
|
||||||
this.my_id = my_id;
|
this.my_id = my_id;
|
||||||
|
this.infohash = infohash;
|
||||||
this.metainfo = metainfo;
|
this.metainfo = metainfo;
|
||||||
_id = ++__id;
|
_id = ++__id;
|
||||||
//_log.debug("Creating a new peer with " + peerID.toString(), new Exception("creating"));
|
//_log.debug("Creating a new peer with " + peerID.toString(), new Exception("creating"));
|
||||||
@ -89,12 +106,14 @@ public class Peer implements Comparable
|
|||||||
* get the remote peer id. To completely start the connection call
|
* get the remote peer id. To completely start the connection call
|
||||||
* the connect() method.
|
* the connect() method.
|
||||||
*
|
*
|
||||||
|
* @param metainfo null if in magnet mode
|
||||||
* @exception IOException when an error occurred during the handshake.
|
* @exception IOException when an error occurred during the handshake.
|
||||||
*/
|
*/
|
||||||
public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, MetaInfo metainfo)
|
public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, byte[] infohash, MetaInfo metainfo)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
this.my_id = my_id;
|
this.my_id = my_id;
|
||||||
|
this.infohash = infohash;
|
||||||
this.metainfo = metainfo;
|
this.metainfo = metainfo;
|
||||||
this.sock = sock;
|
this.sock = sock;
|
||||||
|
|
||||||
@ -102,7 +121,7 @@ public class Peer implements Comparable
|
|||||||
this.peerID = new PeerID(id, sock.getPeerDestination());
|
this.peerID = new PeerID(id, sock.getPeerDestination());
|
||||||
_id = ++__id;
|
_id = ++__id;
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Creating a new peer with " + peerID.toString(), new Exception("creating " + _id));
|
_log.debug("Creating a new peer " + peerID.toString(), new Exception("creating " + _id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -192,7 +211,7 @@ public class Peer implements Comparable
|
|||||||
* If the given BitField is non-null it is send to the peer as first
|
* If the given BitField is non-null it is send to the peer as first
|
||||||
* message.
|
* message.
|
||||||
*/
|
*/
|
||||||
public void runConnection(I2PSnarkUtil util, PeerListener listener, BitField bitfield)
|
public void runConnection(I2PSnarkUtil util, PeerListener listener, BitField bitfield, MagnetState mState)
|
||||||
{
|
{
|
||||||
if (state != null)
|
if (state != null)
|
||||||
throw new IllegalStateException("Peer already started");
|
throw new IllegalStateException("Peer already started");
|
||||||
@ -243,14 +262,29 @@ public class Peer implements Comparable
|
|||||||
_log.debug("Already have din [" + sock + "] with " + toString());
|
_log.debug("Already have din [" + sock + "] with " + toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bad idea?
|
||||||
|
if (metainfo == null && (options & OPTION_EXTENSION) == 0) {
|
||||||
|
if (_log.shouldLog(Log.INFO))
|
||||||
|
_log.info("Peer does not support extensions and we need metainfo, dropping");
|
||||||
|
throw new IOException("Peer does not support extensions and we need metainfo, dropping");
|
||||||
|
}
|
||||||
|
|
||||||
PeerConnectionIn in = new PeerConnectionIn(this, din);
|
PeerConnectionIn in = new PeerConnectionIn(this, din);
|
||||||
PeerConnectionOut out = new PeerConnectionOut(this, dout);
|
PeerConnectionOut out = new PeerConnectionOut(this, dout);
|
||||||
PeerState s = new PeerState(this, listener, metainfo, in, out);
|
PeerState s = new PeerState(this, listener, metainfo, in, out);
|
||||||
|
|
||||||
if ((options & OPTION_EXTENSION) != 0) {
|
if ((options & OPTION_EXTENSION) != 0) {
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Peer supports extensions, sending test message");
|
_log.debug("Peer supports extensions, sending reply message");
|
||||||
out.sendExtension(0, ExtensionHandshake.getPayload());
|
int metasize = metainfo != null ? metainfo.getInfoBytes().length : -1;
|
||||||
|
out.sendExtension(0, ExtensionHandler.getHandshake(metasize));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((options & OPTION_DHT) != 0 && util.getDHT() != null) {
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Peer supports DHT, sending PORT message");
|
||||||
|
int port = util.getDHT().getPort();
|
||||||
|
out.sendPort(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send our bitmap
|
// Send our bitmap
|
||||||
@ -259,6 +293,7 @@ public class Peer implements Comparable
|
|||||||
|
|
||||||
// We are up and running!
|
// We are up and running!
|
||||||
state = s;
|
state = s;
|
||||||
|
magnetState = mState;
|
||||||
listener.connected(this);
|
listener.connected(this);
|
||||||
|
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
@ -303,10 +338,10 @@ public class Peer implements Comparable
|
|||||||
dout.write(19);
|
dout.write(19);
|
||||||
dout.write("BitTorrent protocol".getBytes("UTF-8"));
|
dout.write("BitTorrent protocol".getBytes("UTF-8"));
|
||||||
// Handshake write - options
|
// Handshake write - options
|
||||||
dout.writeLong(OPTION_EXTENSION);
|
// FIXME not if DHT disabled
|
||||||
|
dout.writeLong(OPTION_EXTENSION | OPTION_DHT);
|
||||||
// Handshake write - metainfo hash
|
// Handshake write - metainfo hash
|
||||||
byte[] shared_hash = metainfo.getInfoHash();
|
dout.write(infohash);
|
||||||
dout.write(shared_hash);
|
|
||||||
// Handshake write - peer id
|
// Handshake write - peer id
|
||||||
dout.write(my_id);
|
dout.write(my_id);
|
||||||
dout.flush();
|
dout.flush();
|
||||||
@ -334,7 +369,7 @@ public class Peer implements Comparable
|
|||||||
// Handshake read - metainfo hash
|
// Handshake read - metainfo hash
|
||||||
bs = new byte[20];
|
bs = new byte[20];
|
||||||
din.readFully(bs);
|
din.readFully(bs);
|
||||||
if (!Arrays.equals(shared_hash, bs))
|
if (!Arrays.equals(infohash, bs))
|
||||||
throw new IOException("Unexpected MetaInfo hash");
|
throw new IOException("Unexpected MetaInfo hash");
|
||||||
|
|
||||||
// Handshake read - peer id
|
// Handshake read - peer id
|
||||||
@ -342,8 +377,11 @@ public class Peer implements Comparable
|
|||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Read the remote side's hash and peerID fully from " + toString());
|
_log.debug("Read the remote side's hash and peerID fully from " + toString());
|
||||||
|
|
||||||
|
if (DataHelper.eq(my_id, bs))
|
||||||
|
throw new IOException("Connected to myself");
|
||||||
|
|
||||||
if (options != 0) {
|
if (options != 0) {
|
||||||
// send them something
|
// send them something in runConnection() above
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Peer supports options 0x" + Long.toString(options, 16) + ": " + toString());
|
_log.debug("Peer supports options 0x" + Long.toString(options, 16) + ": " + toString());
|
||||||
}
|
}
|
||||||
@ -351,6 +389,55 @@ public class Peer implements Comparable
|
|||||||
return bs;
|
return bs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @since 0.8.4 */
|
||||||
|
public long getOptions() {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @since 0.8.4 */
|
||||||
|
public Destination getDestination() {
|
||||||
|
if (sock == null)
|
||||||
|
return null;
|
||||||
|
return sock.getPeerDestination();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared state across all peers, callers must sync on returned object
|
||||||
|
* @return non-null
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public MagnetState getMagnetState() {
|
||||||
|
return magnetState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return could be null @since 0.8.4 */
|
||||||
|
public Map<String, BEValue> getHandshakeMap() {
|
||||||
|
return handshakeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @since 0.8.4 */
|
||||||
|
public void setHandshakeMap(Map<String, BEValue> map) {
|
||||||
|
handshakeMap = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @since 0.8.4 */
|
||||||
|
public void sendExtension(int type, byte[] payload) {
|
||||||
|
PeerState s = state;
|
||||||
|
if (s != null)
|
||||||
|
s.out.sendExtension(type, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch from magnet mode to normal mode
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void setMetaInfo(MetaInfo meta) {
|
||||||
|
metainfo = meta;
|
||||||
|
PeerState s = state;
|
||||||
|
if (s != null)
|
||||||
|
s.setMetaInfo(meta);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isConnected()
|
public boolean isConnected()
|
||||||
{
|
{
|
||||||
return state != null;
|
return state != null;
|
||||||
@ -513,14 +600,29 @@ public class Peer implements Comparable
|
|||||||
return (s == null) || s.choked;
|
return (s == null) || s.choked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the counter.
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void downloaded(int size) {
|
||||||
|
downloaded += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the counter.
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void uploaded(int size) {
|
||||||
|
uploaded += size;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of bytes that have been downloaded.
|
* Returns the number of bytes that have been downloaded.
|
||||||
* Can be reset to zero with <code>resetCounters()</code>/
|
* Can be reset to zero with <code>resetCounters()</code>/
|
||||||
*/
|
*/
|
||||||
public long getDownloaded()
|
public long getDownloaded()
|
||||||
{
|
{
|
||||||
PeerState s = state;
|
return downloaded;
|
||||||
return (s != null) ? s.downloaded : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -529,8 +631,7 @@ public class Peer implements Comparable
|
|||||||
*/
|
*/
|
||||||
public long getUploaded()
|
public long getUploaded()
|
||||||
{
|
{
|
||||||
PeerState s = state;
|
return uploaded;
|
||||||
return (s != null) ? s.uploaded : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -538,12 +639,8 @@ public class Peer implements Comparable
|
|||||||
*/
|
*/
|
||||||
public void resetCounters()
|
public void resetCounters()
|
||||||
{
|
{
|
||||||
PeerState s = state;
|
downloaded = 0;
|
||||||
if (s != null)
|
uploaded = 0;
|
||||||
{
|
|
||||||
s.downloaded = 0;
|
|
||||||
s.uploaded = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getInactiveTime() {
|
public long getInactiveTime() {
|
||||||
|
@ -88,12 +88,11 @@ public class PeerAcceptor
|
|||||||
}
|
}
|
||||||
if (coordinator != null) {
|
if (coordinator != null) {
|
||||||
// single torrent capability
|
// single torrent capability
|
||||||
MetaInfo meta = coordinator.getMetaInfo();
|
if (DataHelper.eq(coordinator.getInfoHash(), peerInfoHash)) {
|
||||||
if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) {
|
|
||||||
if (coordinator.needPeers())
|
if (coordinator.needPeers())
|
||||||
{
|
{
|
||||||
Peer peer = new Peer(socket, in, out, coordinator.getID(),
|
Peer peer = new Peer(socket, in, out, coordinator.getID(),
|
||||||
coordinator.getMetaInfo());
|
coordinator.getInfoHash(), coordinator.getMetaInfo());
|
||||||
coordinator.addPeer(peer);
|
coordinator.addPeer(peer);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -101,26 +100,25 @@ public class PeerAcceptor
|
|||||||
} else {
|
} else {
|
||||||
// its for another infohash, but we are only single torrent capable. b0rk.
|
// its for another infohash, but we are only single torrent capable. b0rk.
|
||||||
throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash)
|
throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash)
|
||||||
+ ") while we only support (" + Base64.encode(meta.getInfoHash()) + ")");
|
+ ") while we only support (" + Base64.encode(coordinator.getInfoHash()) + ")");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// multitorrent capable, so lets see what we can handle
|
// multitorrent capable, so lets see what we can handle
|
||||||
for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) {
|
for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) {
|
||||||
PeerCoordinator cur = (PeerCoordinator)iter.next();
|
PeerCoordinator cur = (PeerCoordinator)iter.next();
|
||||||
MetaInfo meta = cur.getMetaInfo();
|
|
||||||
|
if (DataHelper.eq(cur.getInfoHash(), peerInfoHash)) {
|
||||||
if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) {
|
|
||||||
if (cur.needPeers())
|
if (cur.needPeers())
|
||||||
{
|
{
|
||||||
Peer peer = new Peer(socket, in, out, cur.getID(),
|
Peer peer = new Peer(socket, in, out, cur.getID(),
|
||||||
cur.getMetaInfo());
|
cur.getInfoHash(), cur.getMetaInfo());
|
||||||
cur.addPeer(peer);
|
cur.addPeer(peer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Rejecting new peer for " + cur.snark.torrent);
|
_log.debug("Rejecting new peer for " + cur.getName());
|
||||||
socket.close();
|
socket.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,8 @@ class PeerCheckerTask extends TimerTask
|
|||||||
private static final long KILOPERSECOND = 1024*(PeerCoordinator.CHECK_PERIOD/1000);
|
private static final long KILOPERSECOND = 1024*(PeerCoordinator.CHECK_PERIOD/1000);
|
||||||
|
|
||||||
private final PeerCoordinator coordinator;
|
private final PeerCoordinator coordinator;
|
||||||
public I2PSnarkUtil _util;
|
private final I2PSnarkUtil _util;
|
||||||
|
private int _runCount;
|
||||||
|
|
||||||
PeerCheckerTask(I2PSnarkUtil util, PeerCoordinator coordinator)
|
PeerCheckerTask(I2PSnarkUtil util, PeerCoordinator coordinator)
|
||||||
{
|
{
|
||||||
@ -49,12 +50,10 @@ class PeerCheckerTask extends TimerTask
|
|||||||
|
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
|
_runCount++;
|
||||||
List<Peer> peerList = coordinator.peerList();
|
List<Peer> peerList = coordinator.peerList();
|
||||||
if (peerList.isEmpty() || coordinator.halted()) {
|
if (peerList.isEmpty() || coordinator.halted()) {
|
||||||
coordinator.peerCount = 0;
|
|
||||||
coordinator.interestedAndChoking = 0;
|
|
||||||
coordinator.setRateHistory(0, 0);
|
coordinator.setRateHistory(0, 0);
|
||||||
coordinator.uploaders = 0;
|
|
||||||
if (coordinator.halted())
|
if (coordinator.halted())
|
||||||
cancel();
|
cancel();
|
||||||
return;
|
return;
|
||||||
@ -207,6 +206,10 @@ class PeerCheckerTask extends TimerTask
|
|||||||
}
|
}
|
||||||
peer.retransmitRequests();
|
peer.retransmitRequests();
|
||||||
peer.keepAlive();
|
peer.keepAlive();
|
||||||
|
// announce them to local tracker (TrackerClient does this too)
|
||||||
|
if (_util.getDHT() != null && (_runCount % 5) == 0) {
|
||||||
|
_util.getDHT().announce(coordinator.getInfoHash(), peer.getPeerID().getDestHash());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resync actual uploaders value
|
// Resync actual uploaders value
|
||||||
@ -247,8 +250,14 @@ class PeerCheckerTask extends TimerTask
|
|||||||
coordinator.setRateHistory(uploaded, downloaded);
|
coordinator.setRateHistory(uploaded, downloaded);
|
||||||
|
|
||||||
// close out unused files, but we don't need to do it every time
|
// close out unused files, but we don't need to do it every time
|
||||||
if (random.nextInt(4) == 0)
|
Storage storage = coordinator.getStorage();
|
||||||
coordinator.getStorage().cleanRAFs();
|
if (storage != null && (_runCount % 4) == 0) {
|
||||||
|
storage.cleanRAFs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// announce ourselves to local tracker (TrackerClient does this too)
|
||||||
|
if (_util.getDHT() != null && (_runCount % 16) == 0) {
|
||||||
|
_util.getDHT().announce(coordinator.getInfoHash());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,13 @@ class PeerConnectionIn implements Runnable
|
|||||||
private final Peer peer;
|
private final Peer peer;
|
||||||
private final DataInputStream din;
|
private final DataInputStream din;
|
||||||
|
|
||||||
|
// The max length of a complete message in bytes.
|
||||||
|
// The biggest is the piece message, for which the length is the
|
||||||
|
// request size (32K) plus 9. (we could also check if Storage.MAX_PIECES / 8
|
||||||
|
// in the bitfield message is bigger but it's currently 5000/8 = 625 so don't bother)
|
||||||
|
private static final int MAX_MSG_SIZE = Math.max(PeerState.PARTSIZE + 9,
|
||||||
|
MagnetState.CHUNK_SIZE + 100); // 100 for the ext msg dictionary
|
||||||
|
|
||||||
private Thread thread;
|
private Thread thread;
|
||||||
private volatile boolean quit;
|
private volatile boolean quit;
|
||||||
|
|
||||||
@ -77,20 +84,16 @@ class PeerConnectionIn implements Runnable
|
|||||||
int len;
|
int len;
|
||||||
|
|
||||||
// Wait till we hear something...
|
// Wait till we hear something...
|
||||||
// The length of a complete message in bytes.
|
|
||||||
// The biggest is the piece message, for which the length is the
|
|
||||||
// request size (32K) plus 9. (we could also check if Storage.MAX_PIECES / 8
|
|
||||||
// in the bitfield message is bigger but it's currently 5000/8 = 625 so don't bother)
|
|
||||||
int i = din.readInt();
|
int i = din.readInt();
|
||||||
lastRcvd = System.currentTimeMillis();
|
lastRcvd = System.currentTimeMillis();
|
||||||
if (i < 0 || i > PeerState.PARTSIZE + 9)
|
if (i < 0 || i > MAX_MSG_SIZE)
|
||||||
throw new IOException("Unexpected length prefix: " + i);
|
throw new IOException("Unexpected length prefix: " + i);
|
||||||
|
|
||||||
if (i == 0)
|
if (i == 0)
|
||||||
{
|
{
|
||||||
ps.keepAliveMessage();
|
ps.keepAliveMessage();
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received keepalive from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received keepalive from " + peer);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,35 +105,35 @@ class PeerConnectionIn implements Runnable
|
|||||||
case 0:
|
case 0:
|
||||||
ps.chokeMessage(true);
|
ps.chokeMessage(true);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received choke from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received choke from " + peer);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
ps.chokeMessage(false);
|
ps.chokeMessage(false);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received unchoke from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received unchoke from " + peer);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
ps.interestedMessage(true);
|
ps.interestedMessage(true);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received interested from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received interested from " + peer);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
ps.interestedMessage(false);
|
ps.interestedMessage(false);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received not interested from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received not interested from " + peer);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
piece = din.readInt();
|
piece = din.readInt();
|
||||||
ps.haveMessage(piece);
|
ps.haveMessage(piece);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received havePiece(" + piece + ") from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received havePiece(" + piece + ") from " + peer);
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
byte[] bitmap = new byte[i-1];
|
byte[] bitmap = new byte[i-1];
|
||||||
din.readFully(bitmap);
|
din.readFully(bitmap);
|
||||||
ps.bitfieldMessage(bitmap);
|
ps.bitfieldMessage(bitmap);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received bitmap from " + peer + " on " + peer.metainfo.getName() + ": size=" + (i-1) /* + ": " + ps.bitfield */ );
|
_log.debug("Received bitmap from " + peer + ": size=" + (i-1) /* + ": " + ps.bitfield */ );
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
piece = din.readInt();
|
piece = din.readInt();
|
||||||
@ -138,7 +141,7 @@ class PeerConnectionIn implements Runnable
|
|||||||
len = din.readInt();
|
len = din.readInt();
|
||||||
ps.requestMessage(piece, begin, len);
|
ps.requestMessage(piece, begin, len);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received request(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received request(" + piece + "," + begin + ") from " + peer);
|
||||||
break;
|
break;
|
||||||
case 7:
|
case 7:
|
||||||
piece = din.readInt();
|
piece = din.readInt();
|
||||||
@ -152,7 +155,7 @@ class PeerConnectionIn implements Runnable
|
|||||||
din.readFully(piece_bytes, begin, len);
|
din.readFully(piece_bytes, begin, len);
|
||||||
ps.pieceMessage(req);
|
ps.pieceMessage(req);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received data(" + piece + "," + begin + ") from " + peer);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -160,7 +163,7 @@ class PeerConnectionIn implements Runnable
|
|||||||
piece_bytes = new byte[len];
|
piece_bytes = new byte[len];
|
||||||
din.readFully(piece_bytes);
|
din.readFully(piece_bytes);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received UNWANTED data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received UNWANTED data(" + piece + "," + begin + ") from " + peer);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 8:
|
case 8:
|
||||||
@ -169,22 +172,28 @@ class PeerConnectionIn implements Runnable
|
|||||||
len = din.readInt();
|
len = din.readInt();
|
||||||
ps.cancelMessage(piece, begin, len);
|
ps.cancelMessage(piece, begin, len);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received cancel(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received cancel(" + piece + "," + begin + ") from " + peer);
|
||||||
|
break;
|
||||||
|
case 9: // PORT message
|
||||||
|
int port = din.readUnsignedShort();
|
||||||
|
ps.portMessage(port);
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Received port message from " + peer);
|
||||||
break;
|
break;
|
||||||
case 20: // Extension message
|
case 20: // Extension message
|
||||||
int id = din.readUnsignedByte();
|
int id = din.readUnsignedByte();
|
||||||
byte[] payload = new byte[i-2];
|
byte[] payload = new byte[i-2];
|
||||||
din.readFully(payload);
|
din.readFully(payload);
|
||||||
ps.extensionMessage(id, payload);
|
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received extension message from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received extension message from " + peer);
|
||||||
|
ps.extensionMessage(id, payload);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
byte[] bs = new byte[i-1];
|
byte[] bs = new byte[i-1];
|
||||||
din.readFully(bs);
|
din.readFully(bs);
|
||||||
ps.unknownMessage(b, bs);
|
ps.unknownMessage(b, bs);
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Received unknown message from " + peer + " on " + peer.metainfo.getName());
|
_log.debug("Received unknown message from " + peer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,7 +151,7 @@ class PeerConnectionOut implements Runnable
|
|||||||
if (m != null)
|
if (m != null)
|
||||||
{
|
{
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Send " + peer + ": " + m + " on " + peer.metainfo.getName());
|
_log.debug("Send " + peer + ": " + m);
|
||||||
|
|
||||||
// This can block for quite a while.
|
// This can block for quite a while.
|
||||||
// To help get slow peers going, and track the bandwidth better,
|
// To help get slow peers going, and track the bandwidth better,
|
||||||
@ -547,4 +547,12 @@ class PeerConnectionOut implements Runnable
|
|||||||
m.len = bytes.length;
|
m.len = bytes.length;
|
||||||
addMessage(m);
|
addMessage(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @since 0.8.4 */
|
||||||
|
void sendPort(int port) {
|
||||||
|
Message m = new Message();
|
||||||
|
m.type = Message.PORT;
|
||||||
|
m.piece = port;
|
||||||
|
addMessage(m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,24 +36,43 @@ import net.i2p.util.I2PAppThread;
|
|||||||
import net.i2p.util.Log;
|
import net.i2p.util.Log;
|
||||||
import net.i2p.util.SimpleTimer2;
|
import net.i2p.util.SimpleTimer2;
|
||||||
|
|
||||||
|
import org.klomp.snark.dht.DHT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coordinates what peer does what.
|
* Coordinates what peer does what.
|
||||||
*/
|
*/
|
||||||
public class PeerCoordinator implements PeerListener
|
public class PeerCoordinator implements PeerListener
|
||||||
{
|
{
|
||||||
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerCoordinator.class);
|
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerCoordinator.class);
|
||||||
final MetaInfo metainfo;
|
|
||||||
final Storage storage;
|
/**
|
||||||
final Snark snark;
|
* External use by PeerMonitorTask only.
|
||||||
|
* Will be null when in magnet mode.
|
||||||
|
*/
|
||||||
|
MetaInfo metainfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External use by PeerMonitorTask only.
|
||||||
|
* Will be null when in magnet mode.
|
||||||
|
*/
|
||||||
|
Storage storage;
|
||||||
|
private final Snark snark;
|
||||||
|
|
||||||
// package local for access by CheckDownLoadersTask
|
// package local for access by CheckDownLoadersTask
|
||||||
final static long CHECK_PERIOD = 40*1000; // 40 seconds
|
final static long CHECK_PERIOD = 40*1000; // 40 seconds
|
||||||
final static int MAX_UPLOADERS = 6;
|
final static int MAX_UPLOADERS = 6;
|
||||||
|
|
||||||
// Approximation of the number of current uploaders.
|
/**
|
||||||
// Resynced by PeerChecker once in a while.
|
* Approximation of the number of current uploaders.
|
||||||
int uploaders = 0;
|
* Resynced by PeerChecker once in a while.
|
||||||
int interestedAndChoking = 0;
|
* External use by PeerCheckerTask only.
|
||||||
|
*/
|
||||||
|
int uploaders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External use by PeerCheckerTask only.
|
||||||
|
*/
|
||||||
|
int interestedAndChoking;
|
||||||
|
|
||||||
// final static int MAX_DOWNLOADERS = MAX_CONNECTIONS;
|
// final static int MAX_DOWNLOADERS = MAX_CONNECTIONS;
|
||||||
// int downloaders = 0;
|
// int downloaders = 0;
|
||||||
@ -61,19 +80,24 @@ public class PeerCoordinator implements PeerListener
|
|||||||
private long uploaded;
|
private long uploaded;
|
||||||
private long downloaded;
|
private long downloaded;
|
||||||
final static int RATE_DEPTH = 3; // make following arrays RATE_DEPTH long
|
final static int RATE_DEPTH = 3; // make following arrays RATE_DEPTH long
|
||||||
private long uploaded_old[] = {-1,-1,-1};
|
private final long uploaded_old[] = {-1,-1,-1};
|
||||||
private long downloaded_old[] = {-1,-1,-1};
|
private final long downloaded_old[] = {-1,-1,-1};
|
||||||
|
|
||||||
// synchronize on this when changing peers or downloaders
|
/**
|
||||||
// This is a Queue, not a Set, because PeerCheckerTask keeps things in order for choking/unchoking
|
* synchronize on this when changing peers or downloaders.
|
||||||
|
* This is a Queue, not a Set, because PeerCheckerTask keeps things in order for choking/unchoking.
|
||||||
|
* External use by PeerMonitorTask only.
|
||||||
|
*/
|
||||||
final Queue<Peer> peers;
|
final Queue<Peer> peers;
|
||||||
|
|
||||||
/** estimate of the peers, without requiring any synchronization */
|
/** estimate of the peers, without requiring any synchronization */
|
||||||
volatile int peerCount;
|
private volatile int peerCount;
|
||||||
|
|
||||||
/** Timer to handle all periodical tasks. */
|
/** Timer to handle all periodical tasks. */
|
||||||
private final CheckEvent timer;
|
private final CheckEvent timer;
|
||||||
|
|
||||||
private final byte[] id;
|
private final byte[] id;
|
||||||
|
private final byte[] infohash;
|
||||||
|
|
||||||
/** The wanted pieces. We could use a TreeSet but we'd have to clear and re-add everything
|
/** The wanted pieces. We could use a TreeSet but we'd have to clear and re-add everything
|
||||||
* when priorities change.
|
* when priorities change.
|
||||||
@ -85,18 +109,21 @@ public class PeerCoordinator implements PeerListener
|
|||||||
|
|
||||||
private boolean halted = false;
|
private boolean halted = false;
|
||||||
|
|
||||||
|
private final MagnetState magnetState;
|
||||||
private final CoordinatorListener listener;
|
private final CoordinatorListener listener;
|
||||||
public I2PSnarkUtil _util;
|
private final I2PSnarkUtil _util;
|
||||||
private static final Random _random = I2PAppContext.getGlobalContext().random();
|
private static final Random _random = I2PAppContext.getGlobalContext().random();
|
||||||
|
|
||||||
public String trackerProblems = null;
|
/**
|
||||||
public int trackerSeenPeers = 0;
|
* @param metainfo null if in magnet mode
|
||||||
|
* @param storage null if in magnet mode
|
||||||
public PeerCoordinator(I2PSnarkUtil util, byte[] id, MetaInfo metainfo, Storage storage,
|
*/
|
||||||
|
public PeerCoordinator(I2PSnarkUtil util, byte[] id, byte[] infohash, MetaInfo metainfo, Storage storage,
|
||||||
CoordinatorListener listener, Snark torrent)
|
CoordinatorListener listener, Snark torrent)
|
||||||
{
|
{
|
||||||
_util = util;
|
_util = util;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
this.infohash = infohash;
|
||||||
this.metainfo = metainfo;
|
this.metainfo = metainfo;
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
@ -106,6 +133,7 @@ public class PeerCoordinator implements PeerListener
|
|||||||
setWantedPieces();
|
setWantedPieces();
|
||||||
partialPieces = new ArrayList(getMaxConnections() + 1);
|
partialPieces = new ArrayList(getMaxConnections() + 1);
|
||||||
peers = new LinkedBlockingQueue();
|
peers = new LinkedBlockingQueue();
|
||||||
|
magnetState = new MagnetState(infohash, metainfo);
|
||||||
|
|
||||||
// Install a timer to check the uploaders.
|
// Install a timer to check the uploaders.
|
||||||
// Randomize the first start time so multiple tasks are spread out,
|
// Randomize the first start time so multiple tasks are spread out,
|
||||||
@ -133,6 +161,8 @@ public class PeerCoordinator implements PeerListener
|
|||||||
// only called externally from Storage after the double-check fails
|
// only called externally from Storage after the double-check fails
|
||||||
public void setWantedPieces()
|
public void setWantedPieces()
|
||||||
{
|
{
|
||||||
|
if (metainfo == null || storage == null)
|
||||||
|
return;
|
||||||
// Make a list of pieces
|
// Make a list of pieces
|
||||||
synchronized(wantedPieces) {
|
synchronized(wantedPieces) {
|
||||||
wantedPieces.clear();
|
wantedPieces.clear();
|
||||||
@ -153,7 +183,6 @@ public class PeerCoordinator implements PeerListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Storage getStorage() { return storage; }
|
public Storage getStorage() { return storage; }
|
||||||
public CoordinatorListener getListener() { return listener; }
|
|
||||||
|
|
||||||
// for web page detailed stats
|
// for web page detailed stats
|
||||||
public List<Peer> peerList()
|
public List<Peer> peerList()
|
||||||
@ -166,8 +195,16 @@ public class PeerCoordinator implements PeerListener
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return snark.getName();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean completed()
|
public boolean completed()
|
||||||
{
|
{
|
||||||
|
// FIXME return metainfo complete status
|
||||||
|
if (storage == null)
|
||||||
|
return false;
|
||||||
return storage.complete();
|
return storage.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,9 +221,12 @@ public class PeerCoordinator implements PeerListener
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns how many bytes are still needed to get the complete file.
|
* Returns how many bytes are still needed to get the complete file.
|
||||||
|
* @return -1 if in magnet mode
|
||||||
*/
|
*/
|
||||||
public long getLeft()
|
public long getLeft()
|
||||||
{
|
{
|
||||||
|
if (metainfo == null | storage == null)
|
||||||
|
return -1;
|
||||||
// XXX - Only an approximation.
|
// XXX - Only an approximation.
|
||||||
return ((long) storage.needed()) * metainfo.getPieceLength(0);
|
return ((long) storage.needed()) * metainfo.getPieceLength(0);
|
||||||
}
|
}
|
||||||
@ -271,6 +311,12 @@ public class PeerCoordinator implements PeerListener
|
|||||||
return metainfo;
|
return metainfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @since 0.8.4 */
|
||||||
|
public byte[] getInfoHash()
|
||||||
|
{
|
||||||
|
return infohash;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean needPeers()
|
public boolean needPeers()
|
||||||
{
|
{
|
||||||
return !halted && peers.size() < getMaxConnections();
|
return !halted && peers.size() < getMaxConnections();
|
||||||
@ -281,6 +327,8 @@ public class PeerCoordinator implements PeerListener
|
|||||||
* @return 512K: 16; 1M: 11; 2M: 6
|
* @return 512K: 16; 1M: 11; 2M: 6
|
||||||
*/
|
*/
|
||||||
private int getMaxConnections() {
|
private int getMaxConnections() {
|
||||||
|
if (metainfo == null)
|
||||||
|
return 6;
|
||||||
int size = metainfo.getPieceLength(0);
|
int size = metainfo.getPieceLength(0);
|
||||||
int max = _util.getMaxConnections();
|
int max = _util.getMaxConnections();
|
||||||
if (size <= 512*1024 || completed())
|
if (size <= 512*1024 || completed())
|
||||||
@ -355,8 +403,15 @@ public class PeerCoordinator implements PeerListener
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_log.shouldLog(Log.INFO))
|
if (_log.shouldLog(Log.INFO)) {
|
||||||
_log.info("New connection to peer: " + peer + " for " + metainfo.getName());
|
// just for logging
|
||||||
|
String name;
|
||||||
|
if (metainfo == null)
|
||||||
|
name = "Magnet";
|
||||||
|
else
|
||||||
|
name = metainfo.getName();
|
||||||
|
_log.info("New connection to peer: " + peer + " for " + name);
|
||||||
|
}
|
||||||
|
|
||||||
// Add it to the beginning of the list.
|
// Add it to the beginning of the list.
|
||||||
// And try to optimistically make it a uploader.
|
// And try to optimistically make it a uploader.
|
||||||
@ -415,17 +470,27 @@ public class PeerCoordinator implements PeerListener
|
|||||||
|
|
||||||
if (need_more)
|
if (need_more)
|
||||||
{
|
{
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG)) {
|
||||||
_log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + metainfo.getName(), new Exception("add/run"));
|
// just for logging
|
||||||
|
String name;
|
||||||
|
if (metainfo == null)
|
||||||
|
name = "Magnet";
|
||||||
|
else
|
||||||
|
name = metainfo.getName();
|
||||||
|
_log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + name, new Exception("add/run"));
|
||||||
|
}
|
||||||
// Run the peer with us as listener and the current bitfield.
|
// Run the peer with us as listener and the current bitfield.
|
||||||
final PeerListener listener = this;
|
final PeerListener listener = this;
|
||||||
final BitField bitfield = storage.getBitField();
|
final BitField bitfield;
|
||||||
|
if (storage != null)
|
||||||
|
bitfield = storage.getBitField();
|
||||||
|
else
|
||||||
|
bitfield = null;
|
||||||
Runnable r = new Runnable()
|
Runnable r = new Runnable()
|
||||||
{
|
{
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
peer.runConnection(_util, listener, bitfield);
|
peer.runConnection(_util, listener, bitfield, magnetState);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
String threadName = "Snark peer " + peer.toString();
|
String threadName = "Snark peer " + peer.toString();
|
||||||
@ -486,11 +551,6 @@ public class PeerCoordinator implements PeerListener
|
|||||||
interestedAndChoking = count;
|
interestedAndChoking = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getBitMap()
|
|
||||||
{
|
|
||||||
return storage.getBitField().getFieldBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true if we still want the given piece
|
* @return true if we still want the given piece
|
||||||
*/
|
*/
|
||||||
@ -647,6 +707,8 @@ public class PeerCoordinator implements PeerListener
|
|||||||
* @since 0.8.1
|
* @since 0.8.1
|
||||||
*/
|
*/
|
||||||
public void updatePiecePriorities() {
|
public void updatePiecePriorities() {
|
||||||
|
if (storage == null)
|
||||||
|
return;
|
||||||
int[] pri = storage.getPiecePriorities();
|
int[] pri = storage.getPiecePriorities();
|
||||||
if (pri == null) {
|
if (pri == null) {
|
||||||
_log.debug("Updated piece priorities called but no priorities to set?");
|
_log.debug("Updated piece priorities called but no priorities to set?");
|
||||||
@ -713,6 +775,8 @@ public class PeerCoordinator implements PeerListener
|
|||||||
{
|
{
|
||||||
if (halted)
|
if (halted)
|
||||||
return null;
|
return null;
|
||||||
|
if (metainfo == null || storage == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -755,6 +819,8 @@ public class PeerCoordinator implements PeerListener
|
|||||||
*/
|
*/
|
||||||
public boolean gotPiece(Peer peer, int piece, byte[] bs)
|
public boolean gotPiece(Peer peer, int piece, byte[] bs)
|
||||||
{
|
{
|
||||||
|
if (metainfo == null || storage == null)
|
||||||
|
return true;
|
||||||
if (halted) {
|
if (halted) {
|
||||||
_log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
|
_log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
|
||||||
return true; // We don't actually care anymore.
|
return true; // We don't actually care anymore.
|
||||||
@ -951,6 +1017,8 @@ public class PeerCoordinator implements PeerListener
|
|||||||
* @since 0.8.2
|
* @since 0.8.2
|
||||||
*/
|
*/
|
||||||
public PartialPiece getPartialPiece(Peer peer, BitField havePieces) {
|
public PartialPiece getPartialPiece(Peer peer, BitField havePieces) {
|
||||||
|
if (metainfo == null)
|
||||||
|
return null;
|
||||||
synchronized(wantedPieces) {
|
synchronized(wantedPieces) {
|
||||||
// sorts by remaining bytes, least first
|
// sorts by remaining bytes, least first
|
||||||
Collections.sort(partialPieces);
|
Collections.sort(partialPieces);
|
||||||
@ -1057,6 +1125,69 @@ public class PeerCoordinator implements PeerListener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PeerListener callback
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void gotExtension(Peer peer, int id, byte[] bs) {
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Got extension message " + id + " from " + peer);
|
||||||
|
// basic handling done in PeerState... here we just check if we are done
|
||||||
|
if (metainfo == null && id == ExtensionHandler.ID_METADATA) {
|
||||||
|
synchronized (magnetState) {
|
||||||
|
if (magnetState.isComplete()) {
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.warn("Got completed metainfo via extension");
|
||||||
|
metainfo = magnetState.getMetaInfo();
|
||||||
|
listener.gotMetaInfo(this, metainfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (id == ExtensionHandler.ID_HANDSHAKE) {
|
||||||
|
try {
|
||||||
|
if (peer.getHandshakeMap().get("m").getMap().get(ExtensionHandler.TYPE_PEX) != null) {
|
||||||
|
List<Peer> pList = peerList();
|
||||||
|
pList.remove(peer);
|
||||||
|
ExtensionHandler.sendPEX(peer, pList);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// NPE, no map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the storage after transition out of magnet mode
|
||||||
|
* Snark calls this after we call gotMetaInfo()
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void setStorage(Storage stg) {
|
||||||
|
storage = stg;
|
||||||
|
setWantedPieces();
|
||||||
|
// ok we should be in business
|
||||||
|
for (Peer p : peers) {
|
||||||
|
p.setMetaInfo(metainfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PeerListener callback
|
||||||
|
* Tell the DHT to ping it, this will get back the node info
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void gotPort(Peer peer, int port) {
|
||||||
|
DHT dht = _util.getDHT();
|
||||||
|
if (dht != null)
|
||||||
|
dht.ping(peer.getDestination(), port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PeerListener callback
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void gotPeers(Peer peer, List<PeerID> peers) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/** Return number of allowed uploaders for this torrent.
|
/** Return number of allowed uploaders for this torrent.
|
||||||
** Check with Snark to see if we are over the total upload limit.
|
** Check with Snark to see if we are over the total upload limit.
|
||||||
*/
|
*/
|
||||||
@ -1072,6 +1203,14 @@ public class PeerCoordinator implements PeerListener
|
|||||||
return MAX_UPLOADERS;
|
return MAX_UPLOADERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return current
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public int getUploaders() {
|
||||||
|
return uploaders;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean overUpBWLimit()
|
public boolean overUpBWLimit()
|
||||||
{
|
{
|
||||||
if (listener != null)
|
if (listener != null)
|
||||||
|
@ -179,4 +179,32 @@ interface PeerListener
|
|||||||
* @since 0.8.2
|
* @since 0.8.2
|
||||||
*/
|
*/
|
||||||
PartialPiece getPartialPiece(Peer peer, BitField havePieces);
|
PartialPiece getPartialPiece(Peer peer, BitField havePieces);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an extension message is received.
|
||||||
|
*
|
||||||
|
* @param peer the Peer that got the message.
|
||||||
|
* @param id the message ID
|
||||||
|
* @param bs the message payload
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
void gotExtension(Peer peer, int id, byte[] bs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a port message is received.
|
||||||
|
*
|
||||||
|
* @param peer the Peer that got the message.
|
||||||
|
* @param port the port
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
void gotPort(Peer peer, int port);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when peers are received via PEX
|
||||||
|
*
|
||||||
|
* @param peer the Peer that got the message.
|
||||||
|
* @param pIDList the peer IDs (dest hashes)
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
void gotPeers(Peer peer, List<PeerID> pIDList);
|
||||||
}
|
}
|
||||||
|
@ -32,15 +32,13 @@ import java.util.Set;
|
|||||||
import net.i2p.I2PAppContext;
|
import net.i2p.I2PAppContext;
|
||||||
import net.i2p.util.Log;
|
import net.i2p.util.Log;
|
||||||
|
|
||||||
import org.klomp.snark.bencode.BDecoder;
|
|
||||||
import org.klomp.snark.bencode.BEValue;
|
|
||||||
|
|
||||||
class PeerState implements DataLoader
|
class PeerState implements DataLoader
|
||||||
{
|
{
|
||||||
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerState.class);
|
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerState.class);
|
||||||
private final Peer peer;
|
private final Peer peer;
|
||||||
|
/** Fixme, used by Peer.disconnect() to get to the coordinator */
|
||||||
final PeerListener listener;
|
final PeerListener listener;
|
||||||
private final MetaInfo metainfo;
|
private MetaInfo metainfo;
|
||||||
|
|
||||||
// Interesting and choking describes whether we are interested in or
|
// Interesting and choking describes whether we are interested in or
|
||||||
// are choking the other side.
|
// are choking the other side.
|
||||||
@ -52,10 +50,6 @@ class PeerState implements DataLoader
|
|||||||
boolean interested = false;
|
boolean interested = false;
|
||||||
boolean choked = true;
|
boolean choked = true;
|
||||||
|
|
||||||
// Package local for use by Peer.
|
|
||||||
long downloaded;
|
|
||||||
long uploaded;
|
|
||||||
|
|
||||||
/** the pieces the peer has */
|
/** the pieces the peer has */
|
||||||
BitField bitfield;
|
BitField bitfield;
|
||||||
|
|
||||||
@ -74,6 +68,9 @@ class PeerState implements DataLoader
|
|||||||
public final static int PARTSIZE = 16*1024; // outbound request
|
public final static int PARTSIZE = 16*1024; // outbound request
|
||||||
private final static int MAX_PARTSIZE = 64*1024; // Don't let anybody request more than this
|
private final static int MAX_PARTSIZE = 64*1024; // Don't let anybody request more than this
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param metainfo null if in magnet mode
|
||||||
|
*/
|
||||||
PeerState(Peer peer, PeerListener listener, MetaInfo metainfo,
|
PeerState(Peer peer, PeerListener listener, MetaInfo metainfo,
|
||||||
PeerConnectionIn in, PeerConnectionOut out)
|
PeerConnectionIn in, PeerConnectionOut out)
|
||||||
{
|
{
|
||||||
@ -135,6 +132,9 @@ class PeerState implements DataLoader
|
|||||||
{
|
{
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug(peer + " rcv have(" + piece + ")");
|
_log.debug(peer + " rcv have(" + piece + ")");
|
||||||
|
// FIXME we will lose these until we get the metainfo
|
||||||
|
if (metainfo == null)
|
||||||
|
return;
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (piece < 0 || piece >= metainfo.getPieces())
|
if (piece < 0 || piece >= metainfo.getPieces())
|
||||||
{
|
{
|
||||||
@ -172,8 +172,15 @@ class PeerState implements DataLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
// XXX - Check for weird bitfield and disconnect?
|
// XXX - Check for weird bitfield and disconnect?
|
||||||
bitfield = new BitField(bitmap, metainfo.getPieces());
|
// FIXME will have to regenerate the bitfield after we know exactly
|
||||||
|
// how many pieces there are, as we don't know how many spare bits there are.
|
||||||
|
if (metainfo == null)
|
||||||
|
bitfield = new BitField(bitmap, bitmap.length * 8);
|
||||||
|
else
|
||||||
|
bitfield = new BitField(bitmap, metainfo.getPieces());
|
||||||
}
|
}
|
||||||
|
if (metainfo == null)
|
||||||
|
return;
|
||||||
boolean interest = listener.gotBitField(peer, bitfield);
|
boolean interest = listener.gotBitField(peer, bitfield);
|
||||||
setInteresting(interest);
|
setInteresting(interest);
|
||||||
if (bitfield.complete() && !interest) {
|
if (bitfield.complete() && !interest) {
|
||||||
@ -191,6 +198,8 @@ class PeerState implements DataLoader
|
|||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug(peer + " rcv request("
|
_log.debug(peer + " rcv request("
|
||||||
+ piece + ", " + begin + ", " + length + ") ");
|
+ piece + ", " + begin + ", " + length + ") ");
|
||||||
|
if (metainfo == null)
|
||||||
|
return;
|
||||||
if (choking)
|
if (choking)
|
||||||
{
|
{
|
||||||
if (_log.shouldLog(Log.INFO))
|
if (_log.shouldLog(Log.INFO))
|
||||||
@ -273,7 +282,7 @@ class PeerState implements DataLoader
|
|||||||
*/
|
*/
|
||||||
void uploaded(int size)
|
void uploaded(int size)
|
||||||
{
|
{
|
||||||
uploaded += size;
|
peer.uploaded(size);
|
||||||
listener.uploaded(peer, size);
|
listener.uploaded(peer, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +302,7 @@ class PeerState implements DataLoader
|
|||||||
void pieceMessage(Request req)
|
void pieceMessage(Request req)
|
||||||
{
|
{
|
||||||
int size = req.len;
|
int size = req.len;
|
||||||
downloaded += size;
|
peer.downloaded(size);
|
||||||
listener.downloaded(peer, size);
|
listener.downloaded(peer, size);
|
||||||
|
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
@ -314,9 +323,6 @@ class PeerState implements DataLoader
|
|||||||
{
|
{
|
||||||
if (_log.shouldLog(Log.WARN))
|
if (_log.shouldLog(Log.WARN))
|
||||||
_log.warn("Got BAD " + req.piece + " from " + peer);
|
_log.warn("Got BAD " + req.piece + " from " + peer);
|
||||||
// XXX ARGH What now !?!
|
|
||||||
// FIXME Why would we set downloaded to 0?
|
|
||||||
downloaded = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,7 +366,6 @@ class PeerState implements DataLoader
|
|||||||
_log.info("Unrequested 'piece: " + piece + ", "
|
_log.info("Unrequested 'piece: " + piece + ", "
|
||||||
+ begin + ", " + length + "' received from "
|
+ begin + ", " + length + "' received from "
|
||||||
+ peer);
|
+ peer);
|
||||||
downloaded = 0; // XXX - punishment?
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,7 +390,6 @@ class PeerState implements DataLoader
|
|||||||
+ begin + ", "
|
+ begin + ", "
|
||||||
+ length + "' received from "
|
+ length + "' received from "
|
||||||
+ peer);
|
+ peer);
|
||||||
downloaded = 0; // XXX - punishment?
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,22 +489,36 @@ class PeerState implements DataLoader
|
|||||||
/** @since 0.8.2 */
|
/** @since 0.8.2 */
|
||||||
void extensionMessage(int id, byte[] bs)
|
void extensionMessage(int id, byte[] bs)
|
||||||
{
|
{
|
||||||
if (id == 0) {
|
ExtensionHandler.handleMessage(peer, listener, id, bs);
|
||||||
InputStream is = new ByteArrayInputStream(bs);
|
// Peer coord will get metadata from MagnetState,
|
||||||
try {
|
// verify, and then call gotMetaInfo()
|
||||||
BDecoder dec = new BDecoder(is);
|
listener.gotExtension(peer, id, bs);
|
||||||
BEValue bev = dec.bdecodeMap();
|
}
|
||||||
Map map = bev.getMap();
|
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
/**
|
||||||
_log.debug("Got extension handshake message " + bev.toString());
|
* Switch from magnet mode to normal mode
|
||||||
} catch (Exception e) {
|
* @since 0.8.4
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
*/
|
||||||
_log.debug("Failed extension decode", e);
|
public void setMetaInfo(MetaInfo meta) {
|
||||||
}
|
BitField oldBF = bitfield;
|
||||||
|
if (oldBF != null) {
|
||||||
|
if (oldBF.size() != meta.getPieces())
|
||||||
|
// fix bitfield, it was too big by 1-7 bits
|
||||||
|
bitfield = new BitField(oldBF.getFieldBytes(), meta.getPieces());
|
||||||
|
// else no extra
|
||||||
} else {
|
} else {
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
// it will be initialized later
|
||||||
_log.debug("Got extended message type: " + id + " length: " + bs.length);
|
//bitfield = new BitField(meta.getPieces());
|
||||||
}
|
}
|
||||||
|
metainfo = meta;
|
||||||
|
if (bitfield.count() > 0)
|
||||||
|
setInteresting(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @since 0.8.4 */
|
||||||
|
void portMessage(int port)
|
||||||
|
{
|
||||||
|
listener.gotPort(peer, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
void unknownMessage(int type, byte[] bs)
|
void unknownMessage(int type, byte[] bs)
|
||||||
@ -619,6 +637,8 @@ class PeerState implements DataLoader
|
|||||||
// no bitfield yet? nothing to request then.
|
// no bitfield yet? nothing to request then.
|
||||||
if (bitfield == null)
|
if (bitfield == null)
|
||||||
return;
|
return;
|
||||||
|
if (metainfo == null)
|
||||||
|
return;
|
||||||
boolean more_pieces = true;
|
boolean more_pieces = true;
|
||||||
while (more_pieces)
|
while (more_pieces)
|
||||||
{
|
{
|
||||||
|
@ -26,6 +26,7 @@ import java.io.FileInputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
@ -112,6 +113,8 @@ public class Snark
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/******** No, not maintaining a command-line client
|
||||||
|
|
||||||
public static void main(String[] args)
|
public static void main(String[] args)
|
||||||
{
|
{
|
||||||
System.out.println(copyright);
|
System.out.println(copyright);
|
||||||
@ -235,19 +238,27 @@ public class Snark
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
***********/
|
||||||
|
|
||||||
public static final String PROP_MAX_CONNECTIONS = "i2psnark.maxConnections";
|
public static final String PROP_MAX_CONNECTIONS = "i2psnark.maxConnections";
|
||||||
public String torrent;
|
|
||||||
public MetaInfo meta;
|
/** most of these used to be public, use accessors below instead */
|
||||||
public Storage storage;
|
private String torrent;
|
||||||
public PeerCoordinator coordinator;
|
private MetaInfo meta;
|
||||||
public ConnectionAcceptor acceptor;
|
private Storage storage;
|
||||||
public TrackerClient trackerclient;
|
private PeerCoordinator coordinator;
|
||||||
public String rootDataDir = ".";
|
private ConnectionAcceptor acceptor;
|
||||||
public CompleteListener completeListener;
|
private TrackerClient trackerclient;
|
||||||
public boolean stopped;
|
private String rootDataDir = ".";
|
||||||
byte[] id;
|
private final CompleteListener completeListener;
|
||||||
public I2PSnarkUtil _util;
|
private boolean stopped;
|
||||||
private PeerCoordinatorSet _peerCoordinatorSet;
|
private byte[] id;
|
||||||
|
private byte[] infoHash;
|
||||||
|
private final I2PSnarkUtil _util;
|
||||||
|
private final PeerCoordinatorSet _peerCoordinatorSet;
|
||||||
|
private String trackerProblems;
|
||||||
|
private int trackerSeenPeers;
|
||||||
|
|
||||||
|
|
||||||
/** from main() via parseArguments() single torrent */
|
/** from main() via parseArguments() single torrent */
|
||||||
Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
|
Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
|
||||||
@ -306,31 +317,7 @@ public class Snark
|
|||||||
stopped = true;
|
stopped = true;
|
||||||
activity = "Network setup";
|
activity = "Network setup";
|
||||||
|
|
||||||
// "Taking Three as the subject to reason about--
|
id = generateID();
|
||||||
// A convenient number to state--
|
|
||||||
// We add Seven, and Ten, and then multiply out
|
|
||||||
// By One Thousand diminished by Eight.
|
|
||||||
//
|
|
||||||
// "The result we proceed to divide, as you see,
|
|
||||||
// By Nine Hundred and Ninety Two:
|
|
||||||
// Then subtract Seventeen, and the answer must be
|
|
||||||
// Exactly and perfectly true.
|
|
||||||
|
|
||||||
// Create a new ID and fill it with something random. First nine
|
|
||||||
// zeros bytes, then three bytes filled with snark and then
|
|
||||||
// sixteen random bytes.
|
|
||||||
byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17;
|
|
||||||
id = new byte[20];
|
|
||||||
Random random = I2PAppContext.getGlobalContext().random();
|
|
||||||
int i;
|
|
||||||
for (i = 0; i < 9; i++)
|
|
||||||
id[i] = 0;
|
|
||||||
id[i++] = snark;
|
|
||||||
id[i++] = snark;
|
|
||||||
id[i++] = snark;
|
|
||||||
while (i < 20)
|
|
||||||
id[i++] = (byte)random.nextInt(256);
|
|
||||||
|
|
||||||
debug("My peer id: " + PeerID.idencode(id), Snark.INFO);
|
debug("My peer id: " + PeerID.idencode(id), Snark.INFO);
|
||||||
|
|
||||||
int port;
|
int port;
|
||||||
@ -373,6 +360,7 @@ public class Snark
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
meta = new MetaInfo(new BDecoder(in));
|
meta = new MetaInfo(new BDecoder(in));
|
||||||
|
infoHash = meta.getInfoHash();
|
||||||
}
|
}
|
||||||
catch(IOException ioe)
|
catch(IOException ioe)
|
||||||
{
|
{
|
||||||
@ -406,6 +394,8 @@ public class Snark
|
|||||||
*/
|
*/
|
||||||
else
|
else
|
||||||
fatal("Cannot open '" + torrent + "'", ioe);
|
fatal("Cannot open '" + torrent + "'", ioe);
|
||||||
|
} catch (OutOfMemoryError oom) {
|
||||||
|
fatal("ERROR - Out of memory, cannot create torrent " + torrent + ": " + oom.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
if (in != null)
|
if (in != null)
|
||||||
try { in.close(); } catch (IOException ioe) {}
|
try { in.close(); } catch (IOException ioe) {}
|
||||||
@ -457,6 +447,64 @@ public class Snark
|
|||||||
if (start)
|
if (start)
|
||||||
startTorrent();
|
startTorrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* multitorrent, magnet
|
||||||
|
*
|
||||||
|
* @param torrent a fake name for now (not a file name)
|
||||||
|
* @param ih 20-byte info hash
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public Snark(I2PSnarkUtil util, String torrent, byte[] ih,
|
||||||
|
CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
|
||||||
|
ConnectionAcceptor connectionAcceptor, boolean start, String rootDir)
|
||||||
|
{
|
||||||
|
completeListener = complistener;
|
||||||
|
_util = util;
|
||||||
|
_peerCoordinatorSet = peerCoordinatorSet;
|
||||||
|
acceptor = connectionAcceptor;
|
||||||
|
this.torrent = torrent;
|
||||||
|
this.infoHash = ih;
|
||||||
|
this.rootDataDir = rootDir;
|
||||||
|
stopped = true;
|
||||||
|
id = generateID();
|
||||||
|
|
||||||
|
// All we have is an infoHash
|
||||||
|
// meta remains null
|
||||||
|
// storage remains null
|
||||||
|
|
||||||
|
if (start)
|
||||||
|
startTorrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateID() {
|
||||||
|
// "Taking Three as the subject to reason about--
|
||||||
|
// A convenient number to state--
|
||||||
|
// We add Seven, and Ten, and then multiply out
|
||||||
|
// By One Thousand diminished by Eight.
|
||||||
|
//
|
||||||
|
// "The result we proceed to divide, as you see,
|
||||||
|
// By Nine Hundred and Ninety Two:
|
||||||
|
// Then subtract Seventeen, and the answer must be
|
||||||
|
// Exactly and perfectly true.
|
||||||
|
|
||||||
|
// Create a new ID and fill it with something random. First nine
|
||||||
|
// zeros bytes, then three bytes filled with snark and then
|
||||||
|
// sixteen random bytes.
|
||||||
|
byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17;
|
||||||
|
byte[] rv = new byte[20];
|
||||||
|
Random random = I2PAppContext.getGlobalContext().random();
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < 9; i++)
|
||||||
|
rv[i] = 0;
|
||||||
|
rv[i++] = snark;
|
||||||
|
rv[i++] = snark;
|
||||||
|
rv[i++] = snark;
|
||||||
|
while (i < 20)
|
||||||
|
rv[i++] = (byte)random.nextInt(256);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start up contacting peers and querying the tracker
|
* Start up contacting peers and querying the tracker
|
||||||
*/
|
*/
|
||||||
@ -473,7 +521,7 @@ public class Snark
|
|||||||
}
|
}
|
||||||
debug("Starting PeerCoordinator, ConnectionAcceptor, and TrackerClient", NOTICE);
|
debug("Starting PeerCoordinator, ConnectionAcceptor, and TrackerClient", NOTICE);
|
||||||
activity = "Collecting pieces";
|
activity = "Collecting pieces";
|
||||||
coordinator = new PeerCoordinator(_util, id, meta, storage, this, this);
|
coordinator = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this);
|
||||||
if (_peerCoordinatorSet != null) {
|
if (_peerCoordinatorSet != null) {
|
||||||
// multitorrent
|
// multitorrent
|
||||||
_peerCoordinatorSet.add(coordinator);
|
_peerCoordinatorSet.add(coordinator);
|
||||||
@ -486,7 +534,8 @@ public class Snark
|
|||||||
// single torrent
|
// single torrent
|
||||||
acceptor = new ConnectionAcceptor(_util, serversocket, new PeerAcceptor(coordinator));
|
acceptor = new ConnectionAcceptor(_util, serversocket, new PeerAcceptor(coordinator));
|
||||||
}
|
}
|
||||||
trackerclient = new TrackerClient(_util, meta, coordinator);
|
// TODO pass saved closest DHT nodes to the tracker? or direct to the coordinator?
|
||||||
|
trackerclient = new TrackerClient(_util, meta, coordinator, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopped = false;
|
stopped = false;
|
||||||
@ -496,8 +545,7 @@ public class Snark
|
|||||||
// restart safely, so lets build a new one to replace the old
|
// restart safely, so lets build a new one to replace the old
|
||||||
if (_peerCoordinatorSet != null)
|
if (_peerCoordinatorSet != null)
|
||||||
_peerCoordinatorSet.remove(coordinator);
|
_peerCoordinatorSet.remove(coordinator);
|
||||||
PeerCoordinator newCoord = new PeerCoordinator(_util, coordinator.getID(), coordinator.getMetaInfo(),
|
PeerCoordinator newCoord = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this);
|
||||||
coordinator.getStorage(), coordinator.getListener(), this);
|
|
||||||
if (_peerCoordinatorSet != null)
|
if (_peerCoordinatorSet != null)
|
||||||
_peerCoordinatorSet.add(newCoord);
|
_peerCoordinatorSet.add(newCoord);
|
||||||
coordinator = newCoord;
|
coordinator = newCoord;
|
||||||
@ -506,18 +554,17 @@ public class Snark
|
|||||||
if (!trackerclient.started() && !coordinatorChanged) {
|
if (!trackerclient.started() && !coordinatorChanged) {
|
||||||
trackerclient.start();
|
trackerclient.start();
|
||||||
} else if (trackerclient.halted() || coordinatorChanged) {
|
} else if (trackerclient.halted() || coordinatorChanged) {
|
||||||
try
|
if (storage != null) {
|
||||||
{
|
try {
|
||||||
storage.reopen(rootDataDir);
|
storage.reopen(rootDataDir);
|
||||||
}
|
} catch (IOException ioe) {
|
||||||
catch (IOException ioe)
|
try { storage.close(); } catch (IOException ioee) {
|
||||||
{
|
ioee.printStackTrace();
|
||||||
try { storage.close(); } catch (IOException ioee) {
|
}
|
||||||
ioee.printStackTrace();
|
fatal("Could not reopen storage", ioe);
|
||||||
}
|
}
|
||||||
fatal("Could not reopen storage", ioe);
|
}
|
||||||
}
|
TrackerClient newClient = new TrackerClient(_util, meta, coordinator, this);
|
||||||
TrackerClient newClient = new TrackerClient(_util, coordinator.getMetaInfo(), coordinator);
|
|
||||||
if (!trackerclient.halted())
|
if (!trackerclient.halted())
|
||||||
trackerclient.halt();
|
trackerclient.halt();
|
||||||
trackerclient = newClient;
|
trackerclient = newClient;
|
||||||
@ -553,18 +600,238 @@ public class Snark
|
|||||||
_util.disconnect();
|
_util.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Snark parseArguments(String[] args)
|
private static Snark parseArguments(String[] args)
|
||||||
{
|
{
|
||||||
return parseArguments(args, null, null);
|
return parseArguments(args, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return file name of .torrent file (should be full absolute path), or a fake name if in magnet mode.
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return torrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return base name of torrent [filtered version of getMetaInfo.getName()], or a fake name if in magnet mode
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public String getBaseName() {
|
||||||
|
if (storage != null)
|
||||||
|
return storage.getBaseName();
|
||||||
|
return torrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return always will be valid even in magnet mode
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public byte[] getID() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return always will be valid even in magnet mode
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public byte[] getInfoHash() {
|
||||||
|
// should always be the same
|
||||||
|
if (meta != null)
|
||||||
|
return meta.getInfoHash();
|
||||||
|
return infoHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return may be null if in magnet mode
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public MetaInfo getMetaInfo() {
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return may be null if in magnet mode
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public Storage getStorage() {
|
||||||
|
return storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public boolean isStopped() {
|
||||||
|
return stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public long getDownloadRate() {
|
||||||
|
PeerCoordinator coord = coordinator;
|
||||||
|
if (coord != null)
|
||||||
|
return coord.getDownloadRate();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public long getUploadRate() {
|
||||||
|
PeerCoordinator coord = coordinator;
|
||||||
|
if (coord != null)
|
||||||
|
return coord.getUploadRate();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public long getDownloaded() {
|
||||||
|
PeerCoordinator coord = coordinator;
|
||||||
|
if (coord != null)
|
||||||
|
return coord.getDownloaded();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public long getUploaded() {
|
||||||
|
PeerCoordinator coord = coordinator;
|
||||||
|
if (coord != null)
|
||||||
|
return coord.getUploaded();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public int getPeerCount() {
|
||||||
|
PeerCoordinator coord = coordinator;
|
||||||
|
if (coord != null)
|
||||||
|
return coord.getPeerCount();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public List<Peer> getPeerList() {
|
||||||
|
PeerCoordinator coord = coordinator;
|
||||||
|
if (coord != null)
|
||||||
|
return coord.peerList();
|
||||||
|
return Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return String returned from tracker, or null if no error
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public String getTrackerProblems() {
|
||||||
|
return trackerProblems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param p tracker error string or null
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void setTrackerProblems(String p) {
|
||||||
|
trackerProblems = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return count returned from tracker
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public int getTrackerSeenPeers() {
|
||||||
|
return trackerSeenPeers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void setTrackerSeenPeers(int p) {
|
||||||
|
trackerSeenPeers = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void updatePiecePriorities() {
|
||||||
|
PeerCoordinator coord = coordinator;
|
||||||
|
if (coord != null)
|
||||||
|
coord.updatePiecePriorities();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return total of all torrent files, or total of metainfo file if fetching magnet, or -1
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public long getTotalLength() {
|
||||||
|
if (meta != null)
|
||||||
|
return meta.getTotalLength();
|
||||||
|
// FIXME else return metainfo length if available
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return number of pieces still needed (magnet mode or not), or -1 if unknown
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public long getNeeded() {
|
||||||
|
if (storage != null)
|
||||||
|
return storage.needed();
|
||||||
|
if (meta != null)
|
||||||
|
// FIXME subtract chunks we have
|
||||||
|
return meta.getTotalLength();
|
||||||
|
// FIXME fake
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param p the piece number
|
||||||
|
* @return metainfo piece length or 16K if fetching magnet
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public int getPieceLength(int p) {
|
||||||
|
if (meta != null)
|
||||||
|
return meta.getPieceLength(p);
|
||||||
|
return 16*1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return number of pieces
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public int getPieces() {
|
||||||
|
if (meta != null)
|
||||||
|
return meta.getPieces();
|
||||||
|
// FIXME else return metainfo pieces if available
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if restarted
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public boolean restartAcceptor() {
|
||||||
|
if (acceptor == null)
|
||||||
|
return false;
|
||||||
|
acceptor.restart();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets debug, ip and torrent variables then creates a Snark
|
* Sets debug, ip and torrent variables then creates a Snark
|
||||||
* instance. Calls usage(), which terminates the program, if
|
* instance. Calls usage(), which terminates the program, if
|
||||||
* non-valid argument list. The given listeners will be
|
* non-valid argument list. The given listeners will be
|
||||||
* passed to all components that take one.
|
* passed to all components that take one.
|
||||||
*/
|
*/
|
||||||
static Snark parseArguments(String[] args,
|
private static Snark parseArguments(String[] args,
|
||||||
StorageListener slistener,
|
StorageListener slistener,
|
||||||
CoordinatorListener clistener)
|
CoordinatorListener clistener)
|
||||||
{
|
{
|
||||||
@ -719,7 +986,7 @@ public class Snark
|
|||||||
/**
|
/**
|
||||||
* Aborts program abnormally.
|
* Aborts program abnormally.
|
||||||
*/
|
*/
|
||||||
public void fatal(String s)
|
private void fatal(String s)
|
||||||
{
|
{
|
||||||
fatal(s, null);
|
fatal(s, null);
|
||||||
}
|
}
|
||||||
@ -727,7 +994,7 @@ public class Snark
|
|||||||
/**
|
/**
|
||||||
* Aborts program abnormally.
|
* Aborts program abnormally.
|
||||||
*/
|
*/
|
||||||
public void fatal(String s, Throwable t)
|
private void fatal(String s, Throwable t)
|
||||||
{
|
{
|
||||||
_util.debug(s, ERROR, t);
|
_util.debug(s, ERROR, t);
|
||||||
//System.err.println("snark: " + s + ((t == null) ? "" : (": " + t)));
|
//System.err.println("snark: " + s + ((t == null) ? "" : (": " + t)));
|
||||||
@ -751,7 +1018,36 @@ public class Snark
|
|||||||
// System.out.println(peer.toString());
|
// System.out.println(peer.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean allocating = false;
|
/**
|
||||||
|
* Called when the PeerCoordinator got the MetaInfo via magnet.
|
||||||
|
* CoordinatorListener.
|
||||||
|
* Create the storage, tell SnarkManager, and give the storage
|
||||||
|
* back to the coordinator.
|
||||||
|
*
|
||||||
|
* @throws RuntimeException via fatal()
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo) {
|
||||||
|
meta = metainfo;
|
||||||
|
try {
|
||||||
|
storage = new Storage(_util, meta, this);
|
||||||
|
storage.check(rootDataDir);
|
||||||
|
if (completeListener != null) {
|
||||||
|
String newName = completeListener.gotMetaInfo(this);
|
||||||
|
if (newName != null)
|
||||||
|
torrent = newName;
|
||||||
|
// else some horrible problem
|
||||||
|
}
|
||||||
|
coordinator.setStorage(storage);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
if (storage != null) {
|
||||||
|
try { storage.close(); } catch (IOException ioee) {}
|
||||||
|
}
|
||||||
|
fatal("Could not check or create storage", ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean allocating = false;
|
||||||
public void storageCreateFile(Storage storage, String name, long length)
|
public void storageCreateFile(Storage storage, String name, long length)
|
||||||
{
|
{
|
||||||
//if (allocating)
|
//if (allocating)
|
||||||
@ -774,9 +1070,9 @@ public class Snark
|
|||||||
// System.out.println(); // We have all the disk space we need.
|
// System.out.println(); // We have all the disk space we need.
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean allChecked = false;
|
private boolean allChecked = false;
|
||||||
boolean checking = false;
|
private boolean checking = false;
|
||||||
boolean prechecking = true;
|
private boolean prechecking = true;
|
||||||
public void storageChecked(Storage storage, int num, boolean checked)
|
public void storageChecked(Storage storage, int num, boolean checked)
|
||||||
{
|
{
|
||||||
allocating = false;
|
allocating = false;
|
||||||
@ -831,6 +1127,17 @@ public class Snark
|
|||||||
public interface CompleteListener {
|
public interface CompleteListener {
|
||||||
public void torrentComplete(Snark snark);
|
public void torrentComplete(Snark snark);
|
||||||
public void updateStatus(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.
|
||||||
|
*
|
||||||
|
* @return the new name for the torrent or null on error
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public String gotMetaInfo(Snark snark);
|
||||||
|
|
||||||
// not really listeners but the easiest way to get back to an optional SnarkManager
|
// not really listeners but the easiest way to get back to an optional SnarkManager
|
||||||
public long getSavedTorrentTime(Snark snark);
|
public long getSavedTorrentTime(Snark snark);
|
||||||
public BitField getSavedTorrentBitField(Snark snark);
|
public BitField getSavedTorrentBitField(Snark snark);
|
||||||
|
@ -5,6 +5,7 @@ import java.io.FileFilter;
|
|||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@ -16,14 +17,18 @@ import java.util.Set;
|
|||||||
import java.util.StringTokenizer;
|
import java.util.StringTokenizer;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import net.i2p.I2PAppContext;
|
import net.i2p.I2PAppContext;
|
||||||
import net.i2p.data.Base64;
|
import net.i2p.data.Base64;
|
||||||
import net.i2p.data.DataHelper;
|
import net.i2p.data.DataHelper;
|
||||||
|
import net.i2p.util.ConcurrentHashSet;
|
||||||
|
import net.i2p.util.FileUtil;
|
||||||
import net.i2p.util.I2PAppThread;
|
import net.i2p.util.I2PAppThread;
|
||||||
import net.i2p.util.Log;
|
import net.i2p.util.Log;
|
||||||
import net.i2p.util.OrderedProperties;
|
import net.i2p.util.OrderedProperties;
|
||||||
import net.i2p.util.SecureDirectory;
|
import net.i2p.util.SecureDirectory;
|
||||||
|
import net.i2p.util.SecureFileOutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage multiple snarks
|
* Manage multiple snarks
|
||||||
@ -32,8 +37,14 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
private static SnarkManager _instance = new SnarkManager();
|
private static SnarkManager _instance = new SnarkManager();
|
||||||
public static SnarkManager instance() { return _instance; }
|
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;
|
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;
|
||||||
private final Object _addSnarkLock;
|
private final Object _addSnarkLock;
|
||||||
private /* FIXME final FIXME */ File _configFile;
|
private /* FIXME final FIXME */ File _configFile;
|
||||||
private Properties _config;
|
private Properties _config;
|
||||||
@ -57,6 +68,7 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
public static final String PROP_META_PREFIX = "i2psnark.zmeta.";
|
public static final String PROP_META_PREFIX = "i2psnark.zmeta.";
|
||||||
public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
|
public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
|
||||||
public static final String PROP_META_PRIORITY_SUFFIX = ".priority";
|
public static final String PROP_META_PRIORITY_SUFFIX = ".priority";
|
||||||
|
public static final String PROP_META_MAGNET_PREFIX = "i2psnark.magnet.";
|
||||||
|
|
||||||
private static final String CONFIG_FILE = "i2psnark.config";
|
private static final String CONFIG_FILE = "i2psnark.config";
|
||||||
public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops
|
public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops
|
||||||
@ -71,7 +83,8 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
public static final int DEFAULT_MAX_UP_BW = 10;
|
public static final int DEFAULT_MAX_UP_BW = 10;
|
||||||
public static final int DEFAULT_STARTUP_DELAY = 3;
|
public static final int DEFAULT_STARTUP_DELAY = 3;
|
||||||
private SnarkManager() {
|
private SnarkManager() {
|
||||||
_snarks = new HashMap();
|
_snarks = new ConcurrentHashMap();
|
||||||
|
_magnets = new ConcurrentHashSet();
|
||||||
_addSnarkLock = new Object();
|
_addSnarkLock = new Object();
|
||||||
_context = I2PAppContext.getGlobalContext();
|
_context = I2PAppContext.getGlobalContext();
|
||||||
_log = _context.logManager().getLog(SnarkManager.class);
|
_log = _context.logManager().getLog(SnarkManager.class);
|
||||||
@ -90,8 +103,6 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
_running = true;
|
_running = true;
|
||||||
_peerCoordinatorSet = new PeerCoordinatorSet();
|
_peerCoordinatorSet = new PeerCoordinatorSet();
|
||||||
_connectionAcceptor = new ConnectionAcceptor(_util);
|
_connectionAcceptor = new ConnectionAcceptor(_util);
|
||||||
int minutes = getStartupDelayMinutes();
|
|
||||||
_messages.add(_("Adding torrents in {0} minutes", minutes));
|
|
||||||
_monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true);
|
_monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true);
|
||||||
_monitor.start();
|
_monitor.start();
|
||||||
_context.addShutdownTask(new SnarkManagerShutdown());
|
_context.addShutdownTask(new SnarkManagerShutdown());
|
||||||
@ -252,7 +263,9 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS);
|
String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS);
|
||||||
if (ot != null)
|
if (ot != null)
|
||||||
_util.setOpenTrackerString(ot);
|
_util.setOpenTrackerString(ot);
|
||||||
// FIXME set util use open trackers property somehow
|
String useOT = _config.getProperty(I2PSnarkUtil.PROP_USE_OPENTRACKERS);
|
||||||
|
boolean bOT = useOT == null || Boolean.valueOf(useOT).booleanValue();
|
||||||
|
_util.setUseOpenTrackers(bOT);
|
||||||
getDataDir().mkdirs();
|
getDataDir().mkdirs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +334,7 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
_util.setStartupDelay(minutes);
|
_util.setStartupDelay(minutes);
|
||||||
changed = true;
|
changed = true;
|
||||||
_config.setProperty(PROP_STARTUP_DELAY, "" + minutes);
|
_config.setProperty(PROP_STARTUP_DELAY, "" + minutes);
|
||||||
addMessage(_("Startup delay limit changed to {0} minutes", minutes));
|
addMessage(_("Startup delay changed to {0}", DataHelper.formatDuration2(minutes * 60 * 1000)));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -329,7 +342,9 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
int oldI2CPPort = _util.getI2CPPort();
|
int oldI2CPPort = _util.getI2CPPort();
|
||||||
String oldI2CPHost = _util.getI2CPHost();
|
String oldI2CPHost = _util.getI2CPHost();
|
||||||
int port = oldI2CPPort;
|
int port = oldI2CPPort;
|
||||||
try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {}
|
if (i2cpPort != null) {
|
||||||
|
try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {}
|
||||||
|
}
|
||||||
String host = oldI2CPHost;
|
String host = oldI2CPHost;
|
||||||
Map opts = new HashMap();
|
Map opts = new HashMap();
|
||||||
if (i2cpOpts == null) i2cpOpts = "";
|
if (i2cpOpts == null) i2cpOpts = "";
|
||||||
@ -359,7 +374,7 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
Set names = listTorrentFiles();
|
Set names = listTorrentFiles();
|
||||||
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
|
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
|
||||||
Snark snark = getTorrent((String)iter.next());
|
Snark snark = getTorrent((String)iter.next());
|
||||||
if ( (snark != null) && (!snark.stopped) ) {
|
if ( (snark != null) && (!snark.isStopped()) ) {
|
||||||
snarksActive = true;
|
snarksActive = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -398,9 +413,8 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
|
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
|
||||||
String name = (String)iter.next();
|
String name = (String)iter.next();
|
||||||
Snark snark = getTorrent(name);
|
Snark snark = getTorrent(name);
|
||||||
if ( (snark != null) && (snark.acceptor != null) ) {
|
if (snark != null && snark.restartAcceptor()) {
|
||||||
snark.acceptor.restart();
|
addMessage(_("I2CP listener restarted for \"{0}\"", snark.getBaseName()));
|
||||||
addMessage(_("I2CP listener restarted for \"{0}\"", snark.meta.getName()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -422,6 +436,7 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
addMessage(_("Enabled open trackers - torrent restart required to take effect."));
|
addMessage(_("Enabled open trackers - torrent restart required to take effect."));
|
||||||
else
|
else
|
||||||
addMessage(_("Disabled open trackers - torrent restart required to take effect."));
|
addMessage(_("Disabled open trackers - torrent restart required to take effect."));
|
||||||
|
_util.setUseOpenTrackers(useOpenTrackers);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (openTrackers != null) {
|
if (openTrackers != null) {
|
||||||
@ -461,8 +476,13 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
/** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */
|
/** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */
|
||||||
private static final int MAX_FILES_PER_TORRENT = 512;
|
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
|
* Grab the torrent given the (canonical) filename of the .torrent file
|
||||||
@ -478,17 +498,38 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
public Snark getTorrentByBaseName(String filename) {
|
public Snark getTorrentByBaseName(String filename) {
|
||||||
synchronized (_snarks) {
|
synchronized (_snarks) {
|
||||||
for (Snark s : _snarks.values()) {
|
for (Snark s : _snarks.values()) {
|
||||||
if (s.storage.getBaseName().equals(filename))
|
if (s.getBaseName().equals(filename))
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
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); }
|
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) {
|
public void addTorrent(String filename, boolean dontAutoStart) {
|
||||||
if ((!dontAutoStart) && !_util.connected()) {
|
if ((!dontAutoStart) && !_util.connected()) {
|
||||||
addMessage(_("Connecting to I2P"));
|
addMessage(_("Connecting to I2P"));
|
||||||
@ -538,23 +579,26 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
|
|
||||||
if (!TrackerClient.isValidAnnounce(info.getAnnounce())) {
|
if (!TrackerClient.isValidAnnounce(info.getAnnounce())) {
|
||||||
if (_util.shouldUseOpenTrackers() && _util.getOpenTrackers() != null) {
|
if (_util.shouldUseOpenTrackers() && _util.getOpenTrackers() != null) {
|
||||||
addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", will announce to i2p open trackers only", info.getName()));
|
addMessage(_("Warning - No I2P trackers in \"{0}\", will announce to I2P open trackers and DHT only.", info.getName()));
|
||||||
|
} else if (_util.getDHT() != null) {
|
||||||
|
addMessage(_("Warning - No I2P trackers in \"{0}\", and open trackers are disabled, will announce to DHT only.", info.getName()));
|
||||||
} else {
|
} else {
|
||||||
addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", and open trackers are disabled, you must enable open trackers before starting the torrent!", info.getName()));
|
addMessage(_("Warning - No I2P trackers in \"{0}\", and DHT and open trackers are disabled, you should enable open trackers or DHT before starting the torrent.", info.getName()));
|
||||||
dontAutoStart = true;
|
dontAutoStart = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String rejectMessage = locked_validateTorrent(info);
|
String rejectMessage = validateTorrent(info);
|
||||||
if (rejectMessage != null) {
|
if (rejectMessage != null) {
|
||||||
sfile.delete();
|
sfile.delete();
|
||||||
addMessage(rejectMessage);
|
addMessage(rejectMessage);
|
||||||
return;
|
return;
|
||||||
} else {
|
} 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,
|
torrent = new Snark(_util, filename, null, -1, null, null, this,
|
||||||
_peerCoordinatorSet, _connectionAcceptor,
|
_peerCoordinatorSet, _connectionAcceptor,
|
||||||
false, dataDir.getPath());
|
false, dataDir.getPath());
|
||||||
loadSavedFilePriorities(torrent);
|
loadSavedFilePriorities(torrent);
|
||||||
torrent.completeListener = this;
|
|
||||||
synchronized (_snarks) {
|
synchronized (_snarks) {
|
||||||
_snarks.put(filename, torrent);
|
_snarks.put(filename, torrent);
|
||||||
}
|
}
|
||||||
@ -564,6 +608,8 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
if (sfile.exists())
|
if (sfile.exists())
|
||||||
sfile.delete();
|
sfile.delete();
|
||||||
return;
|
return;
|
||||||
|
} catch (OutOfMemoryError oom) {
|
||||||
|
addMessage(_("ERROR - Out of memory, cannot create torrent from {0}", sfile.getName()) + ": " + oom.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
|
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
|
||||||
}
|
}
|
||||||
@ -572,21 +618,164 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ok, snark created, now lets start it up or configure it further
|
// ok, snark created, now lets start it up or configure it further
|
||||||
File f = new File(filename);
|
|
||||||
if (!dontAutoStart && shouldAutoStart()) {
|
if (!dontAutoStart && shouldAutoStart()) {
|
||||||
torrent.startTorrent();
|
torrent.startTorrent();
|
||||||
addMessage(_("Torrent added and started: \"{0}\"", torrent.storage.getBaseName()));
|
addMessage(_("Torrent added and started: \"{0}\"", torrent.getBaseName()));
|
||||||
} else {
|
} else {
|
||||||
addMessage(_("Torrent added: \"{0}\"", torrent.storage.getBaseName()));
|
addMessage(_("Torrent added: \"{0}\"", torrent.getBaseName()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the timestamp for a torrent from the config file
|
* Add a torrent with the info hash alone (magnet / maggot)
|
||||||
|
*
|
||||||
|
* @param name hex or b32 name from the magnet link
|
||||||
|
* @param ih 20 byte info hash
|
||||||
|
* @throws RuntimeException via Snark.fatal()
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void addMagnet(String name, byte[] ih, boolean updateStatus) {
|
||||||
|
Snark torrent = new Snark(_util, name, ih, this,
|
||||||
|
_peerCoordinatorSet, _connectionAcceptor,
|
||||||
|
false, getDataDir().getPath());
|
||||||
|
|
||||||
|
synchronized (_snarks) {
|
||||||
|
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);
|
||||||
|
if (updateStatus)
|
||||||
|
saveMagnetStatus(ih);
|
||||||
|
_snarks.put(name, torrent);
|
||||||
|
}
|
||||||
|
if (shouldAutoStart()) {
|
||||||
|
torrent.startTorrent();
|
||||||
|
addMessage(_("Fetching {0}", name));
|
||||||
|
boolean haveSavedPeers = false;
|
||||||
|
if ((!util().connected()) && !haveSavedPeers) {
|
||||||
|
addMessage(_("We have no saved peers and no other torrents are running. " +
|
||||||
|
"Fetch of {0} will not succeed until you start another torrent.", name));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addMessage(_("Adding {0}", name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop and delete a torrent running in magnet mode
|
||||||
|
*
|
||||||
|
* @param snark a torrent with a fake file name ("Magnet xxxx")
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void deleteMagnet(Snark snark) {
|
||||||
|
synchronized (_snarks) {
|
||||||
|
_snarks.remove(snark.getName());
|
||||||
|
}
|
||||||
|
snark.stopTorrent();
|
||||||
|
_magnets.remove(snark.getName());
|
||||||
|
removeMagnetStatus(snark.getInfoHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
public long getSavedTorrentTime(Snark snark) {
|
||||||
MetaInfo metainfo = snark.meta;
|
byte[] ih = snark.getInfoHash();
|
||||||
byte[] ih = metainfo.getInfoHash();
|
|
||||||
String infohash = Base64.encode(ih);
|
String infohash = Base64.encode(ih);
|
||||||
infohash = infohash.replace('=', '$');
|
infohash = infohash.replace('=', '$');
|
||||||
String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
|
String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
|
||||||
@ -603,10 +792,13 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
/**
|
/**
|
||||||
* Get the saved bitfield for a torrent from the config file.
|
* Get the saved bitfield for a torrent from the config file.
|
||||||
* Convert "." to a full bitfield.
|
* Convert "." to a full bitfield.
|
||||||
|
* A Snark.CompleteListener method.
|
||||||
*/
|
*/
|
||||||
public BitField getSavedTorrentBitField(Snark snark) {
|
public BitField getSavedTorrentBitField(Snark snark) {
|
||||||
MetaInfo metainfo = snark.meta;
|
MetaInfo metainfo = snark.getMetaInfo();
|
||||||
byte[] ih = metainfo.getInfoHash();
|
if (metainfo == null)
|
||||||
|
return null;
|
||||||
|
byte[] ih = snark.getInfoHash();
|
||||||
String infohash = Base64.encode(ih);
|
String infohash = Base64.encode(ih);
|
||||||
infohash = infohash.replace('=', '$');
|
infohash = infohash.replace('=', '$');
|
||||||
String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
|
String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
|
||||||
@ -636,10 +828,13 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
* @since 0.8.1
|
* @since 0.8.1
|
||||||
*/
|
*/
|
||||||
public void loadSavedFilePriorities(Snark snark) {
|
public void loadSavedFilePriorities(Snark snark) {
|
||||||
MetaInfo metainfo = snark.meta;
|
MetaInfo metainfo = snark.getMetaInfo();
|
||||||
|
Storage storage = snark.getStorage();
|
||||||
|
if (metainfo == null || storage == null)
|
||||||
|
return;
|
||||||
if (metainfo.getFiles() == null)
|
if (metainfo.getFiles() == null)
|
||||||
return;
|
return;
|
||||||
byte[] ih = metainfo.getInfoHash();
|
byte[] ih = snark.getInfoHash();
|
||||||
String infohash = Base64.encode(ih);
|
String infohash = Base64.encode(ih);
|
||||||
infohash = infohash.replace('=', '$');
|
infohash = infohash.replace('=', '$');
|
||||||
String pri = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX);
|
String pri = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX);
|
||||||
@ -655,7 +850,7 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
} catch (Throwable t) {}
|
} catch (Throwable t) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
snark.storage.setFilePriorities(rv);
|
storage.setFilePriorities(rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -666,6 +861,8 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
* The time is a standard long converted to string.
|
* The time is a standard long converted to string.
|
||||||
* The status is either a bitfield converted to Base64 or "." for a completed
|
* The status is either a bitfield converted to Base64 or "." for a completed
|
||||||
* torrent to save space in the config file and in memory.
|
* torrent to save space in the config file and in memory.
|
||||||
|
*
|
||||||
|
* @param bitfield non-null
|
||||||
* @param priorities may be null
|
* @param priorities may be null
|
||||||
*/
|
*/
|
||||||
public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
|
public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
|
||||||
@ -709,6 +906,8 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
_config.remove(prop);
|
_config.remove(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO save closest DHT nodes too
|
||||||
|
|
||||||
saveConfig();
|
saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -726,9 +925,33 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Warning - does not validate announce URL - use TrackerClient.isValidAnnounce()
|
* Just remember we have it
|
||||||
|
* @since 0.8.4
|
||||||
*/
|
*/
|
||||||
private String locked_validateTorrent(MetaInfo info) throws IOException {
|
public void saveMagnetStatus(byte[] ih) {
|
||||||
|
String infohash = Base64.encode(ih);
|
||||||
|
infohash = infohash.replace('=', '$');
|
||||||
|
_config.setProperty(PROP_META_MAGNET_PREFIX + infohash, ".");
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the magnet marker from the config file.
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void removeMagnetStatus(byte[] ih) {
|
||||||
|
String infohash = Base64.encode(ih);
|
||||||
|
infohash = infohash.replace('=', '$');
|
||||||
|
_config.remove(PROP_META_MAGNET_PREFIX + infohash);
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 validateTorrent(MetaInfo info) {
|
||||||
List files = info.getFiles();
|
List files = info.getFiles();
|
||||||
if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
|
if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
|
||||||
return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size());
|
return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size());
|
||||||
@ -777,86 +1000,186 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
remaining = _snarks.size();
|
remaining = _snarks.size();
|
||||||
}
|
}
|
||||||
if (torrent != null) {
|
if (torrent != null) {
|
||||||
boolean wasStopped = torrent.stopped;
|
boolean wasStopped = torrent.isStopped();
|
||||||
torrent.stopTorrent();
|
torrent.stopTorrent();
|
||||||
if (remaining == 0) {
|
if (remaining == 0) {
|
||||||
// should we disconnect/reconnect here (taking care to deal with the other thread's
|
// should we disconnect/reconnect here (taking care to deal with the other thread's
|
||||||
// I2PServerSocket.accept() call properly?)
|
// I2PServerSocket.accept() call properly?)
|
||||||
////_util.
|
////_util.
|
||||||
}
|
}
|
||||||
String name;
|
|
||||||
if (torrent.storage != null) {
|
|
||||||
name = torrent.storage.getBaseName();
|
|
||||||
} else {
|
|
||||||
name = sfile.getName();
|
|
||||||
}
|
|
||||||
if (!wasStopped)
|
if (!wasStopped)
|
||||||
addMessage(_("Torrent stopped: \"{0}\"", name));
|
addMessage(_("Torrent stopped: \"{0}\"", torrent.getBaseName()));
|
||||||
}
|
}
|
||||||
return torrent;
|
return torrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the torrent, leaving it on the list of torrents unless told to remove it
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void stopTorrent(Snark torrent, boolean shouldRemove) {
|
||||||
|
if (shouldRemove) {
|
||||||
|
synchronized (_snarks) {
|
||||||
|
_snarks.remove(torrent.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean wasStopped = torrent.isStopped();
|
||||||
|
torrent.stopTorrent();
|
||||||
|
if (!wasStopped)
|
||||||
|
addMessage(_("Torrent stopped: \"{0}\"", torrent.getBaseName()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the torrent and delete the torrent file itself, but leaving the data
|
* Stop the torrent and delete the torrent file itself, but leaving the data
|
||||||
* behind.
|
* behind.
|
||||||
|
* Holds the snarks lock to prevent interference from the DirMonitor.
|
||||||
*/
|
*/
|
||||||
public void removeTorrent(String filename) {
|
public void removeTorrent(String filename) {
|
||||||
Snark torrent = stopTorrent(filename, true);
|
Snark torrent;
|
||||||
if (torrent != null) {
|
// prevent interference by DirMonitor
|
||||||
|
synchronized (_snarks) {
|
||||||
|
torrent = stopTorrent(filename, true);
|
||||||
|
if (torrent == null)
|
||||||
|
return;
|
||||||
File torrentFile = new File(filename);
|
File torrentFile = new File(filename);
|
||||||
torrentFile.delete();
|
torrentFile.delete();
|
||||||
String name;
|
|
||||||
if (torrent.storage != null) {
|
|
||||||
removeTorrentStatus(torrent.storage.getMetaInfo());
|
|
||||||
name = torrent.storage.getBaseName();
|
|
||||||
} else {
|
|
||||||
name = torrentFile.getName();
|
|
||||||
}
|
|
||||||
addMessage(_("Torrent removed: \"{0}\"", name));
|
|
||||||
}
|
}
|
||||||
|
Storage storage = torrent.getStorage();
|
||||||
|
if (storage != null)
|
||||||
|
removeTorrentStatus(storage.getMetaInfo());
|
||||||
|
addMessage(_("Torrent removed: \"{0}\"", torrent.getBaseName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DirMonitor implements Runnable {
|
private class DirMonitor implements Runnable {
|
||||||
public void run() {
|
public void run() {
|
||||||
try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {}
|
// don't bother delaying if auto start is false
|
||||||
// the first message was a "We are starting up in 1m"
|
long delay = 60 * 1000 * getStartupDelayMinutes();
|
||||||
synchronized (_messages) {
|
if (delay > 0 && shouldAutoStart()) {
|
||||||
if (_messages.size() == 1)
|
_messages.add(_("Adding torrents in {0}", DataHelper.formatDuration2(delay)));
|
||||||
_messages.remove(0);
|
try { Thread.sleep(delay); } catch (InterruptedException ie) {}
|
||||||
|
// the first message was a "We are starting up in 1m"
|
||||||
|
synchronized (_messages) {
|
||||||
|
if (_messages.size() == 1)
|
||||||
|
_messages.remove(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// here because we need to delay until I2CP is up
|
// here because we need to delay until I2CP is up
|
||||||
// although the user will see the default until then
|
// although the user will see the default until then
|
||||||
getBWLimit();
|
getBWLimit();
|
||||||
|
boolean doMagnets = true;
|
||||||
while (true) {
|
while (true) {
|
||||||
File dir = getDataDir();
|
File dir = getDataDir();
|
||||||
if (_log.shouldLog(Log.DEBUG))
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
_log.debug("Directory Monitor loop over " + dir.getAbsolutePath());
|
_log.debug("Directory Monitor loop over " + dir.getAbsolutePath());
|
||||||
try {
|
try {
|
||||||
monitorTorrents(dir);
|
// Don't let this interfere with .torrent files being added or deleted
|
||||||
|
synchronized (_snarks) {
|
||||||
|
monitorTorrents(dir);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
_log.error("Error in the DirectoryMonitor", e);
|
_log.error("Error in the DirectoryMonitor", e);
|
||||||
}
|
}
|
||||||
|
if (doMagnets) {
|
||||||
|
addMagnets();
|
||||||
|
doMagnets = false;
|
||||||
|
}
|
||||||
try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
|
try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** two listeners */
|
// Begin Snark.CompleteListeners
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Snark.CompleteListener method.
|
||||||
|
*/
|
||||||
public void torrentComplete(Snark snark) {
|
public void torrentComplete(Snark snark) {
|
||||||
|
MetaInfo meta = snark.getMetaInfo();
|
||||||
|
Storage storage = snark.getStorage();
|
||||||
|
if (meta == null || storage == null)
|
||||||
|
return;
|
||||||
StringBuilder buf = new StringBuilder(256);
|
StringBuilder buf = new StringBuilder(256);
|
||||||
buf.append("<a href=\"/i2psnark/").append(snark.storage.getBaseName());
|
buf.append("<a href=\"/i2psnark/").append(storage.getBaseName());
|
||||||
if (snark.meta.getFiles() != null)
|
if (meta.getFiles() != null)
|
||||||
buf.append('/');
|
buf.append('/');
|
||||||
buf.append("\">").append(snark.storage.getBaseName()).append("</a>");
|
buf.append("\">").append(storage.getBaseName()).append("</a>");
|
||||||
long len = snark.meta.getTotalLength();
|
|
||||||
addMessage(_("Download finished: {0}", buf.toString())); // + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')');
|
addMessage(_("Download finished: {0}", buf.toString())); // + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')');
|
||||||
updateStatus(snark);
|
updateStatus(snark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Snark.CompleteListener method.
|
||||||
|
*/
|
||||||
public void updateStatus(Snark snark) {
|
public void updateStatus(Snark snark) {
|
||||||
saveTorrentStatus(snark.meta, snark.storage.getBitField(), snark.storage.getFilePriorities());
|
MetaInfo meta = snark.getMetaInfo();
|
||||||
|
Storage storage = snark.getStorage();
|
||||||
|
if (meta != null && storage != null)
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @return the new name for the torrent or null on error
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public String 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 null;
|
||||||
|
}
|
||||||
|
saveTorrentStatus(meta, storage.getBitField(), null); // no file priorities
|
||||||
|
String name = (new File(getDataDir(), storage.getBaseName() + ".torrent")).getAbsolutePath();
|
||||||
|
try {
|
||||||
|
synchronized (_snarks) {
|
||||||
|
locked_writeMetaInfo(meta, name);
|
||||||
|
// put it in the list under the new name
|
||||||
|
_snarks.remove(snark.getName());
|
||||||
|
_snarks.put(name, snark);
|
||||||
|
}
|
||||||
|
_magnets.remove(snark.getName());
|
||||||
|
removeMagnetStatus(snark.getInfoHash());
|
||||||
|
addMessage(_("Metainfo received for {0}", snark.getName()));
|
||||||
|
addMessage(_("Starting up torrent {0}", storage.getBaseName()));
|
||||||
|
return name;
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
addMessage(_("Failed to copy torrent file to {0}", name));
|
||||||
|
_log.error("Failed to write torrent file", ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End Snark.CompleteListeners
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all magnets from the config file
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
private void addMagnets() {
|
||||||
|
for (Object o : _config.keySet()) {
|
||||||
|
String k = (String) o;
|
||||||
|
if (k.startsWith(PROP_META_MAGNET_PREFIX)) {
|
||||||
|
String b64 = k.substring(PROP_META_MAGNET_PREFIX.length());
|
||||||
|
b64 = b64.replace('$', '=');
|
||||||
|
byte[] ih = Base64.decode(b64);
|
||||||
|
// ignore value
|
||||||
|
if (ih != null && ih.length == 20)
|
||||||
|
addMagnet("Magnet: " + I2PSnarkUtil.toHex(ih), ih, false);
|
||||||
|
// else remove from config?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void monitorTorrents(File dir) {
|
private void monitorTorrents(File dir) {
|
||||||
String fileNames[] = dir.list(TorrentFilenameFilter.instance());
|
String fileNames[] = dir.list(TorrentFilenameFilter.instance());
|
||||||
List<String> foundNames = new ArrayList(0);
|
List<String> foundNames = new ArrayList(0);
|
||||||
@ -887,6 +1210,8 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Don't remove magnet torrents that don't have a torrent file yet
|
||||||
|
existingNames.removeAll(_magnets);
|
||||||
// now lets see which ones have been removed...
|
// now lets see which ones have been removed...
|
||||||
for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) {
|
for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) {
|
||||||
String name = (String)iter.next();
|
String name = (String)iter.next();
|
||||||
@ -940,12 +1265,12 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
|
|
||||||
/** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */
|
/** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */
|
||||||
public static final String PROP_TRACKERS = "i2psnark.trackers";
|
public static final String PROP_TRACKERS = "i2psnark.trackers";
|
||||||
private static Map trackerMap = null;
|
private static Map<String, String> trackerMap = null;
|
||||||
/** sorted map of name to announceURL=baseURL */
|
/** sorted map of name to announceURL=baseURL */
|
||||||
public Map getTrackers() {
|
public Map<String, String> getTrackers() {
|
||||||
if (trackerMap != null) // only do this once, can't be updated while running
|
if (trackerMap != null) // only do this once, can't be updated while running
|
||||||
return trackerMap;
|
return trackerMap;
|
||||||
Map rv = new TreeMap();
|
Map<String, String> rv = new TreeMap();
|
||||||
String trackers = _config.getProperty(PROP_TRACKERS);
|
String trackers = _config.getProperty(PROP_TRACKERS);
|
||||||
if ( (trackers == null) || (trackers.trim().length() <= 0) )
|
if ( (trackers == null) || (trackers.trim().length() <= 0) )
|
||||||
trackers = _context.getProperty(PROP_TRACKERS);
|
trackers = _context.getProperty(PROP_TRACKERS);
|
||||||
@ -984,9 +1309,10 @@ public class SnarkManager implements Snark.CompleteListener {
|
|||||||
Set names = listTorrentFiles();
|
Set names = listTorrentFiles();
|
||||||
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
|
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
|
||||||
Snark snark = getTorrent((String)iter.next());
|
Snark snark = getTorrent((String)iter.next());
|
||||||
if ( (snark != null) && (!snark.stopped) )
|
if ( (snark != null) && (!snark.isStopped()) )
|
||||||
snark.stopTorrent();
|
snark.stopTorrent();
|
||||||
}
|
}
|
||||||
|
//save magnets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ public class StaticSnark
|
|||||||
//Security.addProvider(gnu);
|
//Security.addProvider(gnu);
|
||||||
|
|
||||||
// And finally call the normal starting point.
|
// And finally call the normal starting point.
|
||||||
Snark.main(args);
|
//Snark.main(args);
|
||||||
|
System.err.println("unsupported");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,9 @@ public class Storage
|
|||||||
* Creates a storage from the existing file or directory together
|
* Creates a storage from the existing file or directory together
|
||||||
* with an appropriate MetaInfo file as can be announced on the
|
* with an appropriate MetaInfo file as can be announced on the
|
||||||
* given announce String location.
|
* given announce String location.
|
||||||
|
*
|
||||||
|
* @param announce may be null
|
||||||
|
* @param listener may be null
|
||||||
*/
|
*/
|
||||||
public Storage(I2PSnarkUtil util, File baseFile, String announce, StorageListener listener)
|
public Storage(I2PSnarkUtil util, File baseFile, String announce, StorageListener listener)
|
||||||
throws IOException
|
throws IOException
|
||||||
@ -97,7 +100,7 @@ public class Storage
|
|||||||
getFiles(baseFile);
|
getFiles(baseFile);
|
||||||
|
|
||||||
long total = 0;
|
long total = 0;
|
||||||
ArrayList lengthsList = new ArrayList();
|
ArrayList<Long> lengthsList = new ArrayList();
|
||||||
for (int i = 0; i < lengths.length; i++)
|
for (int i = 0; i < lengths.length; i++)
|
||||||
{
|
{
|
||||||
long length = lengths[i];
|
long length = lengths[i];
|
||||||
@ -119,10 +122,10 @@ public class Storage
|
|||||||
bitfield = new BitField(pieces);
|
bitfield = new BitField(pieces);
|
||||||
needed = 0;
|
needed = 0;
|
||||||
|
|
||||||
List files = new ArrayList();
|
List<List<String>> files = new ArrayList();
|
||||||
for (int i = 0; i < names.length; i++)
|
for (int i = 0; i < names.length; i++)
|
||||||
{
|
{
|
||||||
List file = new ArrayList();
|
List<String> file = new ArrayList();
|
||||||
StringTokenizer st = new StringTokenizer(names[i], File.separator);
|
StringTokenizer st = new StringTokenizer(names[i], File.separator);
|
||||||
while (st.hasMoreTokens())
|
while (st.hasMoreTokens())
|
||||||
{
|
{
|
||||||
@ -590,7 +593,7 @@ public class Storage
|
|||||||
* Removes 'suspicious' characters from the given file name.
|
* Removes 'suspicious' characters from the given file name.
|
||||||
* http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx
|
* 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(" "))
|
if (name.equals(".") || name.equals(" "))
|
||||||
return "_";
|
return "_";
|
||||||
|
@ -34,9 +34,12 @@ import java.util.Random;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import net.i2p.I2PAppContext;
|
import net.i2p.I2PAppContext;
|
||||||
|
import net.i2p.data.Hash;
|
||||||
import net.i2p.util.I2PAppThread;
|
import net.i2p.util.I2PAppThread;
|
||||||
import net.i2p.util.Log;
|
import net.i2p.util.Log;
|
||||||
|
|
||||||
|
import org.klomp.snark.dht.DHT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Informs metainfo tracker of events and gets new peers for peer
|
* Informs metainfo tracker of events and gets new peers for peer
|
||||||
* coordinator.
|
* coordinator.
|
||||||
@ -63,6 +66,7 @@ public class TrackerClient extends I2PAppThread
|
|||||||
private I2PSnarkUtil _util;
|
private I2PSnarkUtil _util;
|
||||||
private final MetaInfo meta;
|
private final MetaInfo meta;
|
||||||
private final PeerCoordinator coordinator;
|
private final PeerCoordinator coordinator;
|
||||||
|
private final Snark snark;
|
||||||
private final int port;
|
private final int port;
|
||||||
|
|
||||||
private boolean stop;
|
private boolean stop;
|
||||||
@ -70,15 +74,19 @@ public class TrackerClient extends I2PAppThread
|
|||||||
|
|
||||||
private List trackers;
|
private List trackers;
|
||||||
|
|
||||||
public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator)
|
/**
|
||||||
|
* @param meta null if in magnet mode
|
||||||
|
*/
|
||||||
|
public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator, Snark snark)
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
// Set unique name.
|
// Set unique name.
|
||||||
String id = urlencode(coordinator.getID());
|
String id = urlencode(snark.getID());
|
||||||
setName("TrackerClient " + id.substring(id.length() - 12));
|
setName("TrackerClient " + id.substring(id.length() - 12));
|
||||||
_util = util;
|
_util = util;
|
||||||
this.meta = meta;
|
this.meta = meta;
|
||||||
this.coordinator = coordinator;
|
this.coordinator = coordinator;
|
||||||
|
this.snark = snark;
|
||||||
|
|
||||||
this.port = 6881; //(port == -1) ? 9 : port;
|
this.port = 6881; //(port == -1) ? 9 : port;
|
||||||
|
|
||||||
@ -118,11 +126,10 @@ public class TrackerClient extends I2PAppThread
|
|||||||
@Override
|
@Override
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
String infoHash = urlencode(meta.getInfoHash());
|
String infoHash = urlencode(snark.getInfoHash());
|
||||||
String peerID = urlencode(coordinator.getID());
|
String peerID = urlencode(snark.getID());
|
||||||
|
|
||||||
|
|
||||||
_log.debug("Announce: [" + meta.getAnnounce() + "] infoHash: " + infoHash);
|
|
||||||
|
|
||||||
// Construct the list of trackers for this torrent,
|
// Construct the list of trackers for this torrent,
|
||||||
// starting with the primary one listed in the metainfo,
|
// starting with the primary one listed in the metainfo,
|
||||||
// followed by the secondary open trackers
|
// followed by the secondary open trackers
|
||||||
@ -130,12 +137,18 @@ public class TrackerClient extends I2PAppThread
|
|||||||
// the primary tracker, that we don't add it twice.
|
// the primary tracker, that we don't add it twice.
|
||||||
// todo: check for b32 matches as well
|
// todo: check for b32 matches as well
|
||||||
trackers = new ArrayList(2);
|
trackers = new ArrayList(2);
|
||||||
String primary = meta.getAnnounce();
|
String primary = null;
|
||||||
if (isValidAnnounce(primary)) {
|
if (meta != null) {
|
||||||
trackers.add(new Tracker(meta.getAnnounce(), true));
|
primary = meta.getAnnounce();
|
||||||
} else {
|
if (isValidAnnounce(primary)) {
|
||||||
_log.warn("Skipping invalid or non-i2p announce: " + primary);
|
trackers.add(new Tracker(meta.getAnnounce(), true));
|
||||||
|
_log.debug("Announce: [" + primary + "] infoHash: " + infoHash);
|
||||||
|
} else {
|
||||||
|
_log.warn("Skipping invalid or non-i2p announce: " + primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (primary == null)
|
||||||
|
primary = "";
|
||||||
List tlist = _util.getOpenTrackers();
|
List tlist = _util.getOpenTrackers();
|
||||||
if (tlist != null) {
|
if (tlist != null) {
|
||||||
for (int i = 0; i < tlist.size(); i++) {
|
for (int i = 0; i < tlist.size(); i++) {
|
||||||
@ -160,15 +173,17 @@ public class TrackerClient extends I2PAppThread
|
|||||||
continue;
|
continue;
|
||||||
if (primary.startsWith("http://i2p/" + dest))
|
if (primary.startsWith("http://i2p/" + dest))
|
||||||
continue;
|
continue;
|
||||||
trackers.add(new Tracker(url, false));
|
// opentrackers are primary if we don't have primary
|
||||||
|
trackers.add(new Tracker(url, primary.equals("")));
|
||||||
_log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
|
_log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tlist.isEmpty()) {
|
if (trackers.isEmpty()) {
|
||||||
// FIXME really need to get this message to the gui
|
// FIXME really need to get this message to the gui
|
||||||
stop = true;
|
stop = true;
|
||||||
_log.error("No valid trackers for infoHash: " + infoHash);
|
_log.error("No valid trackers for infoHash: " + infoHash);
|
||||||
|
// FIXME keep going if DHT enabled
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +203,9 @@ public class TrackerClient extends I2PAppThread
|
|||||||
Random r = I2PAppContext.getGlobalContext().random();
|
Random r = I2PAppContext.getGlobalContext().random();
|
||||||
while(!stop)
|
while(!stop)
|
||||||
{
|
{
|
||||||
|
// Local DHT tracker announce
|
||||||
|
if (_util.getDHT() != null)
|
||||||
|
_util.getDHT().announce(snark.getInfoHash());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Sleep some minutes...
|
// Sleep some minutes...
|
||||||
@ -200,7 +218,7 @@ public class TrackerClient extends I2PAppThread
|
|||||||
firstTime = false;
|
firstTime = false;
|
||||||
} else if (completed && runStarted)
|
} else if (completed && runStarted)
|
||||||
delay = 3*SLEEP*60*1000 + random;
|
delay = 3*SLEEP*60*1000 + random;
|
||||||
else if (coordinator.trackerProblems != null && ++consecutiveFails < MAX_CONSEC_FAILS)
|
else if (snark.getTrackerProblems() != null && ++consecutiveFails < MAX_CONSEC_FAILS)
|
||||||
delay = INITIAL_SLEEP;
|
delay = INITIAL_SLEEP;
|
||||||
else
|
else
|
||||||
// sleep a while, when we wake up we will contact only the trackers whose intervals have passed
|
// sleep a while, when we wake up we will contact only the trackers whose intervals have passed
|
||||||
@ -221,7 +239,7 @@ public class TrackerClient extends I2PAppThread
|
|||||||
|
|
||||||
uploaded = coordinator.getUploaded();
|
uploaded = coordinator.getUploaded();
|
||||||
downloaded = coordinator.getDownloaded();
|
downloaded = coordinator.getDownloaded();
|
||||||
left = coordinator.getLeft();
|
left = coordinator.getLeft(); // -1 in magnet mode
|
||||||
|
|
||||||
// First time we got a complete download?
|
// First time we got a complete download?
|
||||||
String event;
|
String event;
|
||||||
@ -251,7 +269,7 @@ public class TrackerClient extends I2PAppThread
|
|||||||
uploaded, downloaded, left,
|
uploaded, downloaded, left,
|
||||||
event);
|
event);
|
||||||
|
|
||||||
coordinator.trackerProblems = null;
|
snark.setTrackerProblems(null);
|
||||||
tr.trackerProblems = null;
|
tr.trackerProblems = null;
|
||||||
tr.registerFails = 0;
|
tr.registerFails = 0;
|
||||||
tr.consecutiveFails = 0;
|
tr.consecutiveFails = 0;
|
||||||
@ -260,18 +278,26 @@ public class TrackerClient extends I2PAppThread
|
|||||||
runStarted = true;
|
runStarted = true;
|
||||||
tr.started = true;
|
tr.started = true;
|
||||||
|
|
||||||
Set peers = info.getPeers();
|
Set<Peer> peers = info.getPeers();
|
||||||
tr.seenPeers = info.getPeerCount();
|
tr.seenPeers = info.getPeerCount();
|
||||||
if (coordinator.trackerSeenPeers < tr.seenPeers) // update rising number quickly
|
if (snark.getTrackerSeenPeers() < tr.seenPeers) // update rising number quickly
|
||||||
coordinator.trackerSeenPeers = tr.seenPeers;
|
snark.setTrackerSeenPeers(tr.seenPeers);
|
||||||
if ( (left > 0) && (!completed) ) {
|
|
||||||
|
// pass everybody over to our tracker
|
||||||
|
if (_util.getDHT() != null) {
|
||||||
|
for (Peer peer : peers) {
|
||||||
|
_util.getDHT().announce(snark.getInfoHash(), peer.getPeerID().getDestHash());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( (left != 0) && (!completed) ) {
|
||||||
// we only want to talk to new people if we need things
|
// we only want to talk to new people if we need things
|
||||||
// from them (duh)
|
// from them (duh)
|
||||||
List ordered = new ArrayList(peers);
|
List<Peer> ordered = new ArrayList(peers);
|
||||||
Collections.shuffle(ordered, r);
|
Collections.shuffle(ordered, r);
|
||||||
Iterator it = ordered.iterator();
|
Iterator<Peer> it = ordered.iterator();
|
||||||
while ((!stop) && it.hasNext()) {
|
while ((!stop) && it.hasNext()) {
|
||||||
Peer cur = (Peer)it.next();
|
Peer cur = it.next();
|
||||||
// FIXME if id == us || dest == us continue;
|
// FIXME if id == us || dest == us continue;
|
||||||
// only delay if we actually make an attempt to add peer
|
// only delay if we actually make an attempt to add peer
|
||||||
if(coordinator.addPeer(cur)) {
|
if(coordinator.addPeer(cur)) {
|
||||||
@ -293,12 +319,12 @@ public class TrackerClient extends I2PAppThread
|
|||||||
tr.trackerProblems = ioe.getMessage();
|
tr.trackerProblems = ioe.getMessage();
|
||||||
// don't show secondary tracker problems to the user
|
// don't show secondary tracker problems to the user
|
||||||
if (tr.isPrimary)
|
if (tr.isPrimary)
|
||||||
coordinator.trackerProblems = tr.trackerProblems;
|
snark.setTrackerProblems(tr.trackerProblems);
|
||||||
if (tr.trackerProblems.toLowerCase().startsWith(NOT_REGISTERED)) {
|
if (tr.trackerProblems.toLowerCase().startsWith(NOT_REGISTERED)) {
|
||||||
// Give a guy some time to register it if using opentrackers too
|
// Give a guy some time to register it if using opentrackers too
|
||||||
if (trackers.size() == 1) {
|
if (trackers.size() == 1) {
|
||||||
stop = true;
|
stop = true;
|
||||||
coordinator.snark.stopTorrent();
|
snark.stopTorrent();
|
||||||
} else { // hopefully each on the opentrackers list is really open
|
} else { // hopefully each on the opentrackers list is really open
|
||||||
if (tr.registerFails++ > MAX_REGISTER_FAILS)
|
if (tr.registerFails++ > MAX_REGISTER_FAILS)
|
||||||
tr.stop = true;
|
tr.stop = true;
|
||||||
@ -315,8 +341,47 @@ public class TrackerClient extends I2PAppThread
|
|||||||
maxSeenPeers = tr.seenPeers;
|
maxSeenPeers = tr.seenPeers;
|
||||||
} // *** end of trackers loop here
|
} // *** end of trackers loop here
|
||||||
|
|
||||||
|
// Get peers from DHT
|
||||||
|
// FIXME this needs to be in its own thread
|
||||||
|
if (_util.getDHT() != null && !stop) {
|
||||||
|
int numwant;
|
||||||
|
if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
|
||||||
|
numwant = 1;
|
||||||
|
else
|
||||||
|
numwant = _util.getMaxConnections();
|
||||||
|
List<Hash> hashes = _util.getDHT().getPeers(snark.getInfoHash(), numwant, 2*60*1000);
|
||||||
|
_util.debug("Got " + hashes + " from DHT", Snark.INFO);
|
||||||
|
// announce ourselves while the token is still good
|
||||||
|
// FIXME this needs to be in its own thread
|
||||||
|
if (!stop) {
|
||||||
|
int good = _util.getDHT().announce(snark.getInfoHash(), 8, 5*60*1000);
|
||||||
|
_util.debug("Sent " + good + " good announces to DHT", Snark.INFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now try these peers
|
||||||
|
if ((!stop) && !hashes.isEmpty()) {
|
||||||
|
List<Peer> peers = new ArrayList(hashes.size());
|
||||||
|
for (Hash h : hashes) {
|
||||||
|
PeerID pID = new PeerID(h.getData());
|
||||||
|
peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
|
||||||
|
}
|
||||||
|
Collections.shuffle(peers, r);
|
||||||
|
Iterator<Peer> it = peers.iterator();
|
||||||
|
while ((!stop) && it.hasNext()) {
|
||||||
|
Peer cur = it.next();
|
||||||
|
if (coordinator.addPeer(cur)) {
|
||||||
|
int delay = DELAY_MUL;
|
||||||
|
delay *= r.nextInt(10);
|
||||||
|
delay += DELAY_MIN;
|
||||||
|
try { Thread.sleep(delay); } catch (InterruptedException ie) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// we could try and total the unique peers but that's too hard for now
|
// we could try and total the unique peers but that's too hard for now
|
||||||
coordinator.trackerSeenPeers = maxSeenPeers;
|
snark.setTrackerSeenPeers(maxSeenPeers);
|
||||||
if (!runStarted)
|
if (!runStarted)
|
||||||
_util.debug(" Retrying in one minute...", Snark.DEBUG);
|
_util.debug(" Retrying in one minute...", Snark.DEBUG);
|
||||||
} // *** end of while loop
|
} // *** end of while loop
|
||||||
@ -329,6 +394,9 @@ public class TrackerClient extends I2PAppThread
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
// Local DHT tracker unannounce
|
||||||
|
if (_util.getDHT() != null)
|
||||||
|
_util.getDHT().unannounce(snark.getInfoHash());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// try to contact everybody we can
|
// try to contact everybody we can
|
||||||
@ -351,6 +419,8 @@ public class TrackerClient extends I2PAppThread
|
|||||||
long downloaded, long left, String event)
|
long downloaded, long left, String event)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
|
// What do we send for left in magnet mode? Can we omit it?
|
||||||
|
long tleft = left >= 0 ? left : 1;
|
||||||
String s = tr.announce
|
String s = tr.announce
|
||||||
+ "?info_hash=" + infoHash
|
+ "?info_hash=" + infoHash
|
||||||
+ "&peer_id=" + peerID
|
+ "&peer_id=" + peerID
|
||||||
@ -358,10 +428,10 @@ public class TrackerClient extends I2PAppThread
|
|||||||
+ "&ip=" + _util.getOurIPString() + ".i2p"
|
+ "&ip=" + _util.getOurIPString() + ".i2p"
|
||||||
+ "&uploaded=" + uploaded
|
+ "&uploaded=" + uploaded
|
||||||
+ "&downloaded=" + downloaded
|
+ "&downloaded=" + downloaded
|
||||||
+ "&left=" + left
|
+ "&left=" + tleft
|
||||||
+ "&compact=1" // NOTE: opentracker will return 400 for &compact alone
|
+ "&compact=1" // NOTE: opentracker will return 400 for &compact alone
|
||||||
+ ((! event.equals(NO_EVENT)) ? ("&event=" + event) : "");
|
+ ((! event.equals(NO_EVENT)) ? ("&event=" + event) : "");
|
||||||
if (left <= 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
|
if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
|
||||||
s += "&numwant=0";
|
s += "&numwant=0";
|
||||||
else
|
else
|
||||||
s += "&numwant=" + _util.getMaxConnections();
|
s += "&numwant=" + _util.getMaxConnections();
|
||||||
@ -377,8 +447,8 @@ public class TrackerClient extends I2PAppThread
|
|||||||
try {
|
try {
|
||||||
in = new FileInputStream(fetched);
|
in = new FileInputStream(fetched);
|
||||||
|
|
||||||
TrackerInfo info = new TrackerInfo(in, coordinator.getID(),
|
TrackerInfo info = new TrackerInfo(in, snark.getID(),
|
||||||
coordinator.getMetaInfo());
|
snark.getInfoHash(), snark.getMetaInfo());
|
||||||
_util.debug("TrackerClient response: " + info, Snark.INFO);
|
_util.debug("TrackerClient response: " + info, Snark.INFO);
|
||||||
|
|
||||||
String failure = info.getFailureReason();
|
String failure = info.getFailureReason();
|
||||||
|
@ -46,19 +46,20 @@ public class TrackerInfo
|
|||||||
private int complete;
|
private int complete;
|
||||||
private int incomplete;
|
private int incomplete;
|
||||||
|
|
||||||
public TrackerInfo(InputStream in, byte[] my_id, MetaInfo metainfo)
|
/** @param metainfo may be null */
|
||||||
|
public TrackerInfo(InputStream in, byte[] my_id, byte[] infohash, MetaInfo metainfo)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
this(new BDecoder(in), my_id, metainfo);
|
this(new BDecoder(in), my_id, infohash, metainfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackerInfo(BDecoder be, byte[] my_id, MetaInfo metainfo)
|
private TrackerInfo(BDecoder be, byte[] my_id, byte[] infohash, MetaInfo metainfo)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
this(be.bdecodeMap().getMap(), my_id, metainfo);
|
this(be.bdecodeMap().getMap(), my_id, infohash, metainfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackerInfo(Map m, byte[] my_id, MetaInfo metainfo)
|
private TrackerInfo(Map m, byte[] my_id, byte[] infohash, MetaInfo metainfo)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
BEValue reason = (BEValue)m.get("failure reason");
|
BEValue reason = (BEValue)m.get("failure reason");
|
||||||
@ -84,10 +85,10 @@ public class TrackerInfo
|
|||||||
Set<Peer> p;
|
Set<Peer> p;
|
||||||
try {
|
try {
|
||||||
// One big string (the official compact format)
|
// One big string (the official compact format)
|
||||||
p = getPeers(bePeers.getBytes(), my_id, metainfo);
|
p = getPeers(bePeers.getBytes(), my_id, infohash, metainfo);
|
||||||
} catch (InvalidBEncodingException ibe) {
|
} catch (InvalidBEncodingException ibe) {
|
||||||
// List of Dictionaries or List of Strings
|
// List of Dictionaries or List of Strings
|
||||||
p = getPeers(bePeers.getList(), my_id, metainfo);
|
p = getPeers(bePeers.getList(), my_id, infohash, metainfo);
|
||||||
}
|
}
|
||||||
peers = p;
|
peers = p;
|
||||||
}
|
}
|
||||||
@ -123,7 +124,7 @@ public class TrackerInfo
|
|||||||
******/
|
******/
|
||||||
|
|
||||||
/** List of Dictionaries or List of Strings */
|
/** List of Dictionaries or List of Strings */
|
||||||
private static Set<Peer> getPeers(List<BEValue> l, byte[] my_id, MetaInfo metainfo)
|
private static Set<Peer> getPeers(List<BEValue> l, byte[] my_id, byte[] infohash, MetaInfo metainfo)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
Set<Peer> peers = new HashSet(l.size());
|
Set<Peer> peers = new HashSet(l.size());
|
||||||
@ -144,7 +145,7 @@ public class TrackerInfo
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
peers.add(new Peer(peerID, my_id, metainfo));
|
peers.add(new Peer(peerID, my_id, infohash, metainfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
return peers;
|
return peers;
|
||||||
@ -156,7 +157,7 @@ public class TrackerInfo
|
|||||||
* One big string of concatenated 32-byte hashes
|
* One big string of concatenated 32-byte hashes
|
||||||
* @since 0.8.1
|
* @since 0.8.1
|
||||||
*/
|
*/
|
||||||
private static Set<Peer> getPeers(byte[] l, byte[] my_id, MetaInfo metainfo)
|
private static Set<Peer> getPeers(byte[] l, byte[] my_id, byte[] infohash, MetaInfo metainfo)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
int count = l.length / HASH_LENGTH;
|
int count = l.length / HASH_LENGTH;
|
||||||
@ -172,7 +173,7 @@ public class TrackerInfo
|
|||||||
// won't happen
|
// won't happen
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
peers.add(new Peer(peerID, my_id, metainfo));
|
peers.add(new Peer(peerID, my_id, infohash, metainfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
return peers;
|
return peers;
|
||||||
|
@ -24,6 +24,8 @@ import java.io.UnsupportedEncodingException;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import net.i2p.data.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds different types that a bencoded byte array can represent.
|
* Holds different types that a bencoded byte array can represent.
|
||||||
* You need to call the correct get method to get the correct java
|
* You need to call the correct get method to get the correct java
|
||||||
@ -178,12 +180,37 @@ public class BEValue
|
|||||||
String valueString;
|
String valueString;
|
||||||
if (value instanceof byte[])
|
if (value instanceof byte[])
|
||||||
{
|
{
|
||||||
|
// try to do a nice job for debugging
|
||||||
byte[] bs = (byte[])value;
|
byte[] bs = (byte[])value;
|
||||||
// XXX - Stupid heuristic... and not UTF-8
|
if (bs.length == 0)
|
||||||
if (bs.length <= 12)
|
valueString = "0 bytes";
|
||||||
valueString = new String(bs);
|
else if (bs.length <= 32) {
|
||||||
else
|
StringBuilder buf = new StringBuilder(32);
|
||||||
valueString = "bytes:" + bs.length;
|
boolean bin = false;
|
||||||
|
for (int i = 0; i < bs.length; i++) {
|
||||||
|
int b = bs[i] & 0xff;
|
||||||
|
// no UTF-8
|
||||||
|
if (b < ' ' || b > 0x7e) {
|
||||||
|
bin = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bin && bs.length <= 8) {
|
||||||
|
buf.append(bs.length).append(" bytes: 0x");
|
||||||
|
for (int i = 0; i < bs.length; i++) {
|
||||||
|
int b = bs[i] & 0xff;
|
||||||
|
if (b < 16)
|
||||||
|
buf.append('0');
|
||||||
|
buf.append(Integer.toHexString(b));
|
||||||
|
}
|
||||||
|
} else if (bin) {
|
||||||
|
buf.append(bs.length).append(" bytes: ").append(Base64.encode(bs));
|
||||||
|
} else {
|
||||||
|
buf.append('"').append(new String(bs)).append('"');
|
||||||
|
}
|
||||||
|
valueString = buf.toString();
|
||||||
|
} else
|
||||||
|
valueString = bs.length + " bytes";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
valueString = value.toString();
|
valueString = value.toString();
|
||||||
|
@ -50,6 +50,8 @@ public class BEncoder
|
|||||||
public static void bencode(Object o, OutputStream out)
|
public static void bencode(Object o, OutputStream out)
|
||||||
throws IOException, IllegalArgumentException
|
throws IOException, IllegalArgumentException
|
||||||
{
|
{
|
||||||
|
if (o == null)
|
||||||
|
throw new NullPointerException("Cannot bencode null");
|
||||||
if (o instanceof String)
|
if (o instanceof String)
|
||||||
bencode((String)o, out);
|
bencode((String)o, out);
|
||||||
else if (o instanceof byte[])
|
else if (o instanceof byte[])
|
||||||
@ -59,7 +61,7 @@ public class BEncoder
|
|||||||
else if (o instanceof List)
|
else if (o instanceof List)
|
||||||
bencode((List)o, out);
|
bencode((List)o, out);
|
||||||
else if (o instanceof Map)
|
else if (o instanceof Map)
|
||||||
bencode((Map)o, out);
|
bencode((Map<String, Object>)o, out);
|
||||||
else if (o instanceof BEValue)
|
else if (o instanceof BEValue)
|
||||||
bencode(((BEValue)o).getValue(), out);
|
bencode(((BEValue)o).getValue(), out);
|
||||||
else
|
else
|
||||||
@ -153,7 +155,7 @@ public class BEncoder
|
|||||||
out.write(bs);
|
out.write(bs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] bencode(Map m)
|
public static byte[] bencode(Map<String, Object> m)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -167,20 +169,20 @@ public class BEncoder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void bencode(Map m, OutputStream out) throws IOException
|
public static void bencode(Map<String, Object> m, OutputStream out) throws IOException
|
||||||
{
|
{
|
||||||
out.write('d');
|
out.write('d');
|
||||||
|
|
||||||
// Keys must be sorted. XXX - But is this the correct order?
|
// Keys must be sorted. XXX - But is this the correct order?
|
||||||
Set s = m.keySet();
|
Set<String> s = m.keySet();
|
||||||
List l = new ArrayList(s);
|
List<String> l = new ArrayList(s);
|
||||||
Collections.sort(l);
|
Collections.sort(l);
|
||||||
|
|
||||||
Iterator it = l.iterator();
|
Iterator<String> it = l.iterator();
|
||||||
while(it.hasNext())
|
while(it.hasNext())
|
||||||
{
|
{
|
||||||
// Keys must be Strings.
|
// Keys must be Strings.
|
||||||
String key = (String)it.next();
|
String key = it.next();
|
||||||
Object value = m.get(key);
|
Object value = m.get(key);
|
||||||
bencode(key, out);
|
bencode(key, out);
|
||||||
bencode(value, out);
|
bencode(value, out);
|
||||||
|
83
apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java
Normal file
83
apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package org.klomp.snark.dht;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2010 zzz (zzz@mail.i2p)
|
||||||
|
* GPLv2
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import net.i2p.data.Destination;
|
||||||
|
import net.i2p.data.Hash;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub for KRPC
|
||||||
|
*/
|
||||||
|
public interface DHT {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The UDP port that should be included in a PORT message.
|
||||||
|
*/
|
||||||
|
public int getPort();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping. We don't have a NID yet so the node is presumed
|
||||||
|
* to be absent from our DHT.
|
||||||
|
* Non-blocking, does not wait for pong.
|
||||||
|
* If and when the pong is received the node will be inserted in our DHT.
|
||||||
|
*/
|
||||||
|
public void ping(Destination dest, int port);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get peers for a torrent.
|
||||||
|
* Blocking!
|
||||||
|
* Caller should run in a thread.
|
||||||
|
*
|
||||||
|
* @param ih the Info Hash (torrent)
|
||||||
|
* @param max maximum number of peers to return
|
||||||
|
* @param maxWait the maximum time to wait (ms) must be > 0
|
||||||
|
* @return list or empty list (never null)
|
||||||
|
*/
|
||||||
|
public List<Hash> getPeers(byte[] ih, int max, long maxWait);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Announce to ourselves.
|
||||||
|
* Non-blocking.
|
||||||
|
*
|
||||||
|
* @param ih the Info Hash (torrent)
|
||||||
|
*/
|
||||||
|
public void announce(byte[] ih);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Announce somebody else we know about.
|
||||||
|
* Non-blocking.
|
||||||
|
*
|
||||||
|
* @param ih the Info Hash (torrent)
|
||||||
|
* @param peer the peer's Hash
|
||||||
|
*/
|
||||||
|
public void announce(byte[] ih, byte[] peerHash);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove reference to ourselves in the local tracker.
|
||||||
|
* Use when shutting down the torrent locally.
|
||||||
|
* Non-blocking.
|
||||||
|
*
|
||||||
|
* @param ih the Info Hash (torrent)
|
||||||
|
*/
|
||||||
|
public void unannounce(byte[] ih);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Announce to the closest DHT peers.
|
||||||
|
* Blocking unless maxWait <= 0
|
||||||
|
* Caller should run in a thread.
|
||||||
|
* This also automatically announces ourself to our local tracker.
|
||||||
|
* For best results do a getPeers() first so we have tokens.
|
||||||
|
*
|
||||||
|
* @param ih the Info Hash (torrent)
|
||||||
|
* @param maxWait the maximum total time to wait (ms) or 0 to do all in parallel and return immediately.
|
||||||
|
* @return the number of successful announces, not counting ourselves.
|
||||||
|
*/
|
||||||
|
public int announce(byte[] ih, int max, long maxWait);
|
||||||
|
}
|
@ -6,6 +6,7 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.text.Collator;
|
import java.text.Collator;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -26,6 +27,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import net.i2p.I2PAppContext;
|
import net.i2p.I2PAppContext;
|
||||||
|
import net.i2p.data.Base32;
|
||||||
import net.i2p.data.Base64;
|
import net.i2p.data.Base64;
|
||||||
import net.i2p.data.DataHelper;
|
import net.i2p.data.DataHelper;
|
||||||
import net.i2p.util.FileUtil;
|
import net.i2p.util.FileUtil;
|
||||||
@ -33,6 +35,7 @@ import net.i2p.util.I2PAppThread;
|
|||||||
import net.i2p.util.Log;
|
import net.i2p.util.Log;
|
||||||
import net.i2p.util.SecureFileOutputStream;
|
import net.i2p.util.SecureFileOutputStream;
|
||||||
|
|
||||||
|
import org.klomp.snark.I2PSnarkUtil;
|
||||||
import org.klomp.snark.MetaInfo;
|
import org.klomp.snark.MetaInfo;
|
||||||
import org.klomp.snark.Peer;
|
import org.klomp.snark.Peer;
|
||||||
import org.klomp.snark.Snark;
|
import org.klomp.snark.Snark;
|
||||||
@ -58,8 +61,13 @@ public class I2PSnarkServlet extends Default {
|
|||||||
private Resource _resourceBase;
|
private Resource _resourceBase;
|
||||||
private String _themePath;
|
private String _themePath;
|
||||||
private String _imgPath;
|
private String _imgPath;
|
||||||
|
private String _lastAnnounceURL = "";
|
||||||
|
|
||||||
public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
|
public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
|
||||||
|
/** BEP 9 */
|
||||||
|
private static final String MAGNET = "magnet:?xt=urn:btih:";
|
||||||
|
/** http://sponge.i2p/files/maggotspec.txt */
|
||||||
|
private static final String MAGGOT = "maggot://";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(ServletConfig cfg) throws ServletException {
|
public void init(ServletConfig cfg) throws ServletException {
|
||||||
@ -153,7 +161,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
resp.setCharacterEncoding("UTF-8");
|
resp.setCharacterEncoding("UTF-8");
|
||||||
resp.setContentType("text/html; charset=UTF-8");
|
resp.setContentType("text/html; charset=UTF-8");
|
||||||
Resource resource = getResource(pathInContext);
|
Resource resource = getResource(pathInContext);
|
||||||
if (resource == null || (!resource.exists()) || !resource.isDirectory()) {
|
if (resource == null || (!resource.exists())) {
|
||||||
resp.sendError(HttpResponse.__404_Not_Found);
|
resp.sendError(HttpResponse.__404_Not_Found);
|
||||||
} else {
|
} else {
|
||||||
String base = URI.addPaths(req.getRequestURI(), "/");
|
String base = URI.addPaths(req.getRequestURI(), "/");
|
||||||
@ -376,7 +384,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
for (int i = 0; i < snarks.size(); i++) {
|
for (int i = 0; i < snarks.size(); i++) {
|
||||||
Snark snark = (Snark)snarks.get(i);
|
Snark snark = (Snark)snarks.get(i);
|
||||||
boolean showDebug = "2".equals(peerParam);
|
boolean showDebug = "2".equals(peerParam);
|
||||||
boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.meta.getInfoHash()).equals(peerParam);
|
boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.getInfoHash()).equals(peerParam);
|
||||||
displaySnark(out, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug);
|
displaySnark(out, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -478,10 +486,12 @@ public class I2PSnarkServlet extends Default {
|
|||||||
if (newURL != null) {
|
if (newURL != null) {
|
||||||
if (newURL.startsWith("http://")) {
|
if (newURL.startsWith("http://")) {
|
||||||
_manager.addMessage(_("Fetching {0}", urlify(newURL)));
|
_manager.addMessage(_("Fetching {0}", urlify(newURL)));
|
||||||
I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add");
|
I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add", true);
|
||||||
fetch.start();
|
fetch.start();
|
||||||
|
} else if (newURL.startsWith(MAGNET) || newURL.startsWith(MAGGOT)) {
|
||||||
|
addMagnet(newURL);
|
||||||
} else {
|
} else {
|
||||||
_manager.addMessage(_("Invalid URL - must start with http://"));
|
_manager.addMessage(_("Invalid URL: Must start with \"http://\", \"{0}\", or \"{1}\"", MAGNET, MAGGOT));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// no file or URL specified
|
// no file or URL specified
|
||||||
@ -494,8 +504,8 @@ public class I2PSnarkServlet extends Default {
|
|||||||
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
||||||
String name = (String)iter.next();
|
String name = (String)iter.next();
|
||||||
Snark snark = _manager.getTorrent(name);
|
Snark snark = _manager.getTorrent(name);
|
||||||
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
|
if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
|
||||||
_manager.stopTorrent(name, false);
|
_manager.stopTorrent(snark, false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -508,11 +518,9 @@ public class I2PSnarkServlet extends Default {
|
|||||||
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
|
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
|
||||||
for (String name : _manager.listTorrentFiles()) {
|
for (String name : _manager.listTorrentFiles()) {
|
||||||
Snark snark = _manager.getTorrent(name);
|
Snark snark = _manager.getTorrent(name);
|
||||||
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
|
if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
|
||||||
snark.startTorrent();
|
snark.startTorrent();
|
||||||
if (snark.storage != null)
|
_manager.addMessage(_("Starting up torrent {0}", snark.getBaseName()));
|
||||||
name = snark.storage.getBaseName();
|
|
||||||
_manager.addMessage(_("Starting up torrent {0}", name));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -526,8 +534,15 @@ public class I2PSnarkServlet extends Default {
|
|||||||
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
||||||
String name = (String)iter.next();
|
String name = (String)iter.next();
|
||||||
Snark snark = _manager.getTorrent(name);
|
Snark snark = _manager.getTorrent(name);
|
||||||
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
|
if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
|
||||||
_manager.stopTorrent(name, true);
|
MetaInfo meta = snark.getMetaInfo();
|
||||||
|
if (meta == null) {
|
||||||
|
// magnet - remove and delete are the same thing
|
||||||
|
_manager.deleteMagnet(snark);
|
||||||
|
_manager.addMessage(_("Magnet deleted: {0}", name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_manager.stopTorrent(snark, true);
|
||||||
// should we delete the torrent file?
|
// should we delete the torrent file?
|
||||||
// yeah, need to, otherwise it'll get autoadded again (at the moment
|
// yeah, need to, otherwise it'll get autoadded again (at the moment
|
||||||
File f = new File(name);
|
File f = new File(name);
|
||||||
@ -546,13 +561,20 @@ public class I2PSnarkServlet extends Default {
|
|||||||
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
|
||||||
String name = (String)iter.next();
|
String name = (String)iter.next();
|
||||||
Snark snark = _manager.getTorrent(name);
|
Snark snark = _manager.getTorrent(name);
|
||||||
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
|
if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
|
||||||
_manager.stopTorrent(name, true);
|
MetaInfo meta = snark.getMetaInfo();
|
||||||
|
if (meta == null) {
|
||||||
|
// magnet - remove and delete are the same thing
|
||||||
|
_manager.deleteMagnet(snark);
|
||||||
|
_manager.addMessage(_("Magnet deleted: {0}", name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_manager.stopTorrent(snark, true);
|
||||||
File f = new File(name);
|
File f = new File(name);
|
||||||
f.delete();
|
f.delete();
|
||||||
_manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath()));
|
_manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath()));
|
||||||
List files = snark.meta.getFiles();
|
List files = meta.getFiles();
|
||||||
String dataFile = snark.meta.getName();
|
String dataFile = snark.getBaseName();
|
||||||
f = new File(_manager.getDataDir(), dataFile);
|
f = new File(_manager.getDataDir(), dataFile);
|
||||||
if (files == null) { // single file torrent
|
if (files == null) { // single file torrent
|
||||||
if (f.delete())
|
if (f.delete())
|
||||||
@ -612,23 +634,23 @@ public class I2PSnarkServlet extends Default {
|
|||||||
if (announceURL == null || announceURL.length() <= 0)
|
if (announceURL == null || announceURL.length() <= 0)
|
||||||
_manager.addMessage(_("Error creating torrent - you must select a tracker"));
|
_manager.addMessage(_("Error creating torrent - you must select a tracker"));
|
||||||
else if (baseFile.exists()) {
|
else if (baseFile.exists()) {
|
||||||
|
_lastAnnounceURL = announceURL;
|
||||||
|
if (announceURL.equals("none"))
|
||||||
|
announceURL = null;
|
||||||
try {
|
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);
|
Storage s = new Storage(_manager.util(), baseFile, announceURL, null);
|
||||||
s.create();
|
s.create();
|
||||||
s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
|
s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
|
||||||
MetaInfo info = s.getMetaInfo();
|
MetaInfo info = s.getMetaInfo();
|
||||||
File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent");
|
File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
|
||||||
if (torrentFile.exists())
|
// FIXME is the storage going to stay around thanks to the info reference?
|
||||||
throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath());
|
// now add it, but don't automatically start it
|
||||||
_manager.saveTorrentStatus(info, s.getBitField(), null); // so addTorrent won't recheck
|
_manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), true);
|
||||||
// DirMonitor could grab this first, maybe hold _snarks lock?
|
|
||||||
FileOutputStream out = new FileOutputStream(torrentFile);
|
|
||||||
out.write(info.getTorrentData());
|
|
||||||
out.close();
|
|
||||||
_manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
|
_manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
|
||||||
// now fire it up, but don't automatically seed it
|
if (announceURL != null)
|
||||||
_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()));
|
||||||
_manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
|
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
_manager.addMessage(_("Error creating a torrent for \"{0}\"", baseFile.getAbsolutePath()) + ": " + ioe.getMessage());
|
_manager.addMessage(_("Error creating a torrent for \"{0}\"", baseFile.getAbsolutePath()) + ": " + ioe.getMessage());
|
||||||
}
|
}
|
||||||
@ -643,8 +665,8 @@ public class I2PSnarkServlet extends Default {
|
|||||||
List snarks = getSortedSnarks(req);
|
List snarks = getSortedSnarks(req);
|
||||||
for (int i = 0; i < snarks.size(); i++) {
|
for (int i = 0; i < snarks.size(); i++) {
|
||||||
Snark snark = (Snark)snarks.get(i);
|
Snark snark = (Snark)snarks.get(i);
|
||||||
if (!snark.stopped)
|
if (!snark.isStopped())
|
||||||
_manager.stopTorrent(snark.torrent, false);
|
_manager.stopTorrent(snark, false);
|
||||||
}
|
}
|
||||||
if (_manager.util().connected()) {
|
if (_manager.util().connected()) {
|
||||||
// Give the stopped announces time to get out
|
// Give the stopped announces time to get out
|
||||||
@ -657,7 +679,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
List snarks = getSortedSnarks(req);
|
List snarks = getSortedSnarks(req);
|
||||||
for (int i = 0; i < snarks.size(); i++) {
|
for (int i = 0; i < snarks.size(); i++) {
|
||||||
Snark snark = (Snark)snarks.get(i);
|
Snark snark = (Snark)snarks.get(i);
|
||||||
if (snark.stopped)
|
if (snark.isStopped())
|
||||||
snark.startTorrent();
|
snark.startTorrent();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -725,7 +747,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
private static final int MAX_DISPLAYED_ERROR_LENGTH = 43;
|
private static final int MAX_DISPLAYED_ERROR_LENGTH = 43;
|
||||||
private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers,
|
private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers,
|
||||||
boolean isDegraded, boolean noThinsp, boolean showDebug) throws IOException {
|
boolean isDegraded, boolean noThinsp, boolean showDebug) throws IOException {
|
||||||
String filename = snark.torrent;
|
String filename = snark.getName();
|
||||||
File f = new File(filename);
|
File f = new File(filename);
|
||||||
filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name
|
filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name
|
||||||
int i = filename.lastIndexOf(".torrent");
|
int i = filename.lastIndexOf(".torrent");
|
||||||
@ -736,28 +758,21 @@ public class I2PSnarkServlet extends Default {
|
|||||||
fullFilename = new String(filename);
|
fullFilename = new String(filename);
|
||||||
filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "…";
|
filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "…";
|
||||||
}
|
}
|
||||||
long total = snark.meta.getTotalLength();
|
long total = snark.getTotalLength();
|
||||||
// Early typecast, avoid possibly overflowing a temp integer
|
// Early typecast, avoid possibly overflowing a temp integer
|
||||||
long remaining = (long) snark.storage.needed() * (long) snark.meta.getPieceLength(0);
|
long remaining = (long) snark.getNeeded() * (long) snark.getPieceLength(0);
|
||||||
if (remaining > total)
|
if (remaining > total)
|
||||||
remaining = total;
|
remaining = total;
|
||||||
long downBps = 0;
|
long downBps = snark.getDownloadRate();
|
||||||
long upBps = 0;
|
long upBps = snark.getUploadRate();
|
||||||
if (snark.coordinator != null) {
|
|
||||||
downBps = snark.coordinator.getDownloadRate();
|
|
||||||
upBps = snark.coordinator.getUploadRate();
|
|
||||||
}
|
|
||||||
long remainingSeconds;
|
long remainingSeconds;
|
||||||
if (downBps > 0)
|
if (downBps > 0)
|
||||||
remainingSeconds = remaining / downBps;
|
remainingSeconds = remaining / downBps;
|
||||||
else
|
else
|
||||||
remainingSeconds = -1;
|
remainingSeconds = -1;
|
||||||
boolean isRunning = !snark.stopped;
|
boolean isRunning = !snark.isStopped();
|
||||||
long uploaded = 0;
|
long uploaded = snark.getUploaded();
|
||||||
if (snark.coordinator != null) {
|
stats[0] += snark.getDownloaded();
|
||||||
uploaded = snark.coordinator.getUploaded();
|
|
||||||
stats[0] += snark.coordinator.getDownloaded();
|
|
||||||
}
|
|
||||||
stats[1] += uploaded;
|
stats[1] += uploaded;
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
stats[2] += downBps;
|
stats[2] += downBps;
|
||||||
@ -765,25 +780,22 @@ public class I2PSnarkServlet extends Default {
|
|||||||
}
|
}
|
||||||
stats[5] += total;
|
stats[5] += total;
|
||||||
|
|
||||||
boolean isValid = snark.meta != null;
|
MetaInfo meta = snark.getMetaInfo();
|
||||||
boolean singleFile = snark.meta.getFiles() == null;
|
// isValid means isNotMagnet
|
||||||
|
boolean isValid = meta != null;
|
||||||
|
boolean isMultiFile = isValid && meta.getFiles() != null;
|
||||||
|
|
||||||
String err = null;
|
String err = snark.getTrackerProblems();
|
||||||
int curPeers = 0;
|
int curPeers = snark.getPeerCount();
|
||||||
int knownPeers = 0;
|
stats[4] += curPeers;
|
||||||
if (snark.coordinator != null) {
|
int knownPeers = Math.max(curPeers, snark.getTrackerSeenPeers());
|
||||||
err = snark.coordinator.trackerProblems;
|
|
||||||
curPeers = snark.coordinator.getPeerCount();
|
|
||||||
stats[4] += curPeers;
|
|
||||||
knownPeers = Math.max(curPeers, snark.coordinator.trackerSeenPeers);
|
|
||||||
}
|
|
||||||
|
|
||||||
String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
|
String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
|
||||||
String statusString;
|
String statusString;
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
if (isRunning && curPeers > 0 && !showPeers)
|
if (isRunning && curPeers > 0 && !showPeers)
|
||||||
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") +
|
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") +
|
||||||
": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
|
": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
|
||||||
curPeers + thinsp(noThinsp) +
|
curPeers + thinsp(noThinsp) +
|
||||||
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
|
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
|
||||||
else if (isRunning)
|
else if (isRunning)
|
||||||
@ -796,10 +808,10 @@ public class I2PSnarkServlet extends Default {
|
|||||||
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") +
|
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") +
|
||||||
"<br>" + err;
|
"<br>" + err;
|
||||||
}
|
}
|
||||||
} else if (remaining <= 0) {
|
} else if (remaining == 0) { // < 0 means no meta size yet
|
||||||
if (isRunning && curPeers > 0 && !showPeers)
|
if (isRunning && curPeers > 0 && !showPeers)
|
||||||
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "seeding.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Seeding") +
|
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "seeding.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Seeding") +
|
||||||
": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
|
": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
|
||||||
curPeers + thinsp(noThinsp) +
|
curPeers + thinsp(noThinsp) +
|
||||||
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
|
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
|
||||||
else if (isRunning)
|
else if (isRunning)
|
||||||
@ -811,7 +823,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
} else {
|
} else {
|
||||||
if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
|
if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
|
||||||
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("OK") +
|
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("OK") +
|
||||||
": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
|
": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
|
||||||
curPeers + thinsp(noThinsp) +
|
curPeers + thinsp(noThinsp) +
|
||||||
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
|
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
|
||||||
else if (isRunning && curPeers > 0 && downBps > 0)
|
else if (isRunning && curPeers > 0 && downBps > 0)
|
||||||
@ -820,7 +832,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
ngettext("1 peer", "{0} peers", knownPeers);
|
ngettext("1 peer", "{0} peers", knownPeers);
|
||||||
else if (isRunning && curPeers > 0 && !showPeers)
|
else if (isRunning && curPeers > 0 && !showPeers)
|
||||||
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Stalled") +
|
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Stalled") +
|
||||||
": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" +
|
": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
|
||||||
curPeers + thinsp(noThinsp) +
|
curPeers + thinsp(noThinsp) +
|
||||||
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
|
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
|
||||||
else if (isRunning && curPeers > 0)
|
else if (isRunning && curPeers > 0)
|
||||||
@ -841,41 +853,24 @@ public class I2PSnarkServlet extends Default {
|
|||||||
out.write(statusString + "</td>\n\t");
|
out.write(statusString + "</td>\n\t");
|
||||||
|
|
||||||
out.write("<td class=\"" + rowClass + "\">");
|
out.write("<td class=\"" + rowClass + "\">");
|
||||||
// temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash
|
if (isValid) {
|
||||||
String announce = snark.meta.getAnnounce();
|
StringBuilder buf = new StringBuilder(128);
|
||||||
if (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") ||
|
buf.append("<a href=\"").append(snark.getBaseName())
|
||||||
announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/") || announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/")) {
|
.append("/\" title=\"").append(_("Torrent details"))
|
||||||
Map trackers = _manager.getTrackers();
|
.append("\"><img alt=\"").append(_("Info")).append("\" border=\"0\" src=\"")
|
||||||
for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
|
.append(_imgPath).append("details.png\"></a>");
|
||||||
Map.Entry entry = (Map.Entry)iter.next();
|
out.write(buf.toString());
|
||||||
String name = (String)entry.getKey();
|
|
||||||
String baseURL = (String)entry.getValue();
|
|
||||||
if (!(baseURL.startsWith(announce) || // vvv hack for non-b64 announce in list vvv
|
|
||||||
(announce.startsWith("http://lnQ6yoBT") && baseURL.startsWith("http://tracker2.postman.i2p/")) ||
|
|
||||||
(announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/") && baseURL.startsWith("http://tracker2.postman.i2p/"))))
|
|
||||||
continue;
|
|
||||||
int e = baseURL.indexOf('=');
|
|
||||||
if (e < 0)
|
|
||||||
continue;
|
|
||||||
baseURL = baseURL.substring(e + 1);
|
|
||||||
out.write("<a href=\"" + baseURL + "details.php?dllist=1&filelist=1&info_hash=");
|
|
||||||
out.write(TrackerClient.urlencode(snark.meta.getInfoHash()));
|
|
||||||
out.write("\" title=\"" + _("Details at {0} tracker", name) + "\" target=\"_blank\">");
|
|
||||||
out.write("<img alt=\"" + _("Info") + "\" border=\"0\" src=\"" + _imgPath + "details.png\">");
|
|
||||||
out.write("</a>");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out.write("</td>\n<td class=\"" + rowClass + "\">");
|
out.write("</td>\n<td class=\"" + rowClass + "\">");
|
||||||
StringBuilder buf = null;
|
StringBuilder buf = null;
|
||||||
if (remaining == 0 || snark.meta.getFiles() != null) {
|
if (remaining == 0 || isMultiFile) {
|
||||||
buf = new StringBuilder(128);
|
buf = new StringBuilder(128);
|
||||||
buf.append("<a href=\"").append(snark.storage.getBaseName());
|
buf.append("<a href=\"").append(snark.getBaseName());
|
||||||
if (snark.meta.getFiles() != null)
|
if (isMultiFile)
|
||||||
buf.append('/');
|
buf.append('/');
|
||||||
buf.append("\" title=\"");
|
buf.append("\" title=\"");
|
||||||
if (snark.meta.getFiles() != null)
|
if (isMultiFile)
|
||||||
buf.append(_("View files"));
|
buf.append(_("View files"));
|
||||||
else
|
else
|
||||||
buf.append(_("Open file"));
|
buf.append(_("Open file"));
|
||||||
@ -883,21 +878,23 @@ public class I2PSnarkServlet extends Default {
|
|||||||
out.write(buf.toString());
|
out.write(buf.toString());
|
||||||
}
|
}
|
||||||
String icon;
|
String icon;
|
||||||
if (snark.meta.getFiles() != null)
|
if (isMultiFile)
|
||||||
icon = "folder";
|
icon = "folder";
|
||||||
|
else if (isValid)
|
||||||
|
icon = toIcon(meta.getName());
|
||||||
else
|
else
|
||||||
icon = toIcon(snark.meta.getName());
|
icon = "magnet";
|
||||||
if (remaining == 0 || snark.meta.getFiles() != null) {
|
if (remaining == 0 || isMultiFile) {
|
||||||
out.write(toImg(icon, _("Open")));
|
out.write(toImg(icon, _("Open")));
|
||||||
out.write("</a>");
|
out.write("</a>");
|
||||||
} else {
|
} else {
|
||||||
out.write(toImg(icon));
|
out.write(toImg(icon));
|
||||||
}
|
}
|
||||||
out.write("</td><td class=\"snarkTorrentName " + rowClass + "\">");
|
out.write("</td><td class=\"snarkTorrentName " + rowClass + "\">");
|
||||||
if (remaining == 0 || snark.meta.getFiles() != null)
|
if (remaining == 0 || isMultiFile)
|
||||||
out.write(buf.toString());
|
out.write(buf.toString());
|
||||||
out.write(filename);
|
out.write(filename);
|
||||||
if (remaining == 0 || snark.meta.getFiles() != null)
|
if (remaining == 0 || isMultiFile)
|
||||||
out.write("</a>");
|
out.write("</a>");
|
||||||
|
|
||||||
out.write("<td align=\"right\" class=\"snarkTorrentETA " + rowClass + "\">");
|
out.write("<td align=\"right\" class=\"snarkTorrentETA " + rowClass + "\">");
|
||||||
@ -907,24 +904,26 @@ public class I2PSnarkServlet extends Default {
|
|||||||
out.write("<td align=\"right\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
|
out.write("<td align=\"right\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
|
||||||
if (remaining > 0)
|
if (remaining > 0)
|
||||||
out.write(formatSize(total-remaining) + thinsp(noThinsp) + formatSize(total));
|
out.write(formatSize(total-remaining) + thinsp(noThinsp) + formatSize(total));
|
||||||
else
|
else if (remaining == 0)
|
||||||
out.write(formatSize(total)); // 3GB
|
out.write(formatSize(total)); // 3GB
|
||||||
|
else
|
||||||
|
out.write("??"); // no meta size yet
|
||||||
out.write("</td>\n\t");
|
out.write("</td>\n\t");
|
||||||
out.write("<td align=\"right\" class=\"snarkTorrentUploaded " + rowClass + "\">");
|
out.write("<td align=\"right\" class=\"snarkTorrentUploaded " + rowClass + "\">");
|
||||||
if(isRunning)
|
if(isRunning && isValid)
|
||||||
out.write(formatSize(uploaded));
|
out.write(formatSize(uploaded));
|
||||||
out.write("</td>\n\t");
|
out.write("</td>\n\t");
|
||||||
out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">");
|
out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">");
|
||||||
if(isRunning && remaining > 0)
|
if(isRunning && remaining != 0)
|
||||||
out.write(formatSize(downBps) + "ps");
|
out.write(formatSize(downBps) + "ps");
|
||||||
out.write("</td>\n\t");
|
out.write("</td>\n\t");
|
||||||
out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">");
|
out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">");
|
||||||
if(isRunning)
|
if(isRunning && isValid)
|
||||||
out.write(formatSize(upBps) + "ps");
|
out.write(formatSize(upBps) + "ps");
|
||||||
out.write("</td>\n\t");
|
out.write("</td>\n\t");
|
||||||
out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">");
|
out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">");
|
||||||
String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.meta.getInfoHash());
|
String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.getInfoHash());
|
||||||
String b64 = Base64.encode(snark.meta.getInfoHash());
|
String b64 = Base64.encode(snark.getInfoHash());
|
||||||
if (showPeers)
|
if (showPeers)
|
||||||
parameters = parameters + "&p=1";
|
parameters = parameters + "&p=1";
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
@ -939,7 +938,6 @@ public class I2PSnarkServlet extends Default {
|
|||||||
if (isDegraded)
|
if (isDegraded)
|
||||||
out.write("</a>");
|
out.write("</a>");
|
||||||
} else {
|
} else {
|
||||||
if (isValid) {
|
|
||||||
if (isDegraded)
|
if (isDegraded)
|
||||||
out.write("<a href=\"/i2psnark/?action=Start_" + b64 + "&nonce=" + _nonce + "\"><img title=\"");
|
out.write("<a href=\"/i2psnark/?action=Start_" + b64 + "&nonce=" + _nonce + "\"><img title=\"");
|
||||||
else
|
else
|
||||||
@ -950,24 +948,25 @@ public class I2PSnarkServlet extends Default {
|
|||||||
out.write("\">");
|
out.write("\">");
|
||||||
if (isDegraded)
|
if (isDegraded)
|
||||||
out.write("</a>");
|
out.write("</a>");
|
||||||
}
|
|
||||||
|
|
||||||
if (isDegraded)
|
if (isValid) {
|
||||||
out.write("<a href=\"/i2psnark/?action=Remove_" + b64 + "&nonce=" + _nonce + "\"><img title=\"");
|
if (isDegraded)
|
||||||
else
|
out.write("<a href=\"/i2psnark/?action=Remove_" + b64 + "&nonce=" + _nonce + "\"><img title=\"");
|
||||||
out.write("<input type=\"image\" name=\"action_Remove_" + b64 + "\" value=\"foo\" title=\"");
|
else
|
||||||
out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
|
out.write("<input type=\"image\" name=\"action\" value=\"Remove_" + b64 + "\" title=\"");
|
||||||
out.write("\" onclick=\"if (!confirm('");
|
out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
|
||||||
// Can't figure out how to escape double quotes inside the onclick string.
|
out.write("\" onclick=\"if (!confirm('");
|
||||||
// Single quotes in translate strings with parameters must be doubled.
|
// Can't figure out how to escape double quotes inside the onclick string.
|
||||||
// Then the remaining single quite must be escaped
|
// Single quotes in translate strings with parameters must be doubled.
|
||||||
out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
|
// Then the remaining single quite must be escaped
|
||||||
out.write("')) { return false; }\"");
|
out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
|
||||||
out.write(" src=\"" + _imgPath + "remove.png\" alt=\"");
|
out.write("')) { return false; }\"");
|
||||||
out.write(_("Remove"));
|
out.write(" src=\"" + _imgPath + "remove.png\" alt=\"");
|
||||||
out.write("\">");
|
out.write(_("Remove"));
|
||||||
if (isDegraded)
|
out.write("\">");
|
||||||
out.write("</a>");
|
if (isDegraded)
|
||||||
|
out.write("</a>");
|
||||||
|
}
|
||||||
|
|
||||||
if (isDegraded)
|
if (isDegraded)
|
||||||
out.write("<a href=\"/i2psnark/?action=Delete_" + b64 + "&nonce=" + _nonce + "\"><img title=\"");
|
out.write("<a href=\"/i2psnark/?action=Delete_" + b64 + "&nonce=" + _nonce + "\"><img title=\"");
|
||||||
@ -989,7 +988,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
out.write("</td>\n</tr>\n");
|
out.write("</td>\n</tr>\n");
|
||||||
|
|
||||||
if(showPeers && isRunning && curPeers > 0) {
|
if(showPeers && isRunning && curPeers > 0) {
|
||||||
List<Peer> peers = snark.coordinator.peerList();
|
List<Peer> peers = snark.getPeerList();
|
||||||
if (!showDebug)
|
if (!showDebug)
|
||||||
Collections.sort(peers, new PeerComparator());
|
Collections.sort(peers, new PeerComparator());
|
||||||
for (Peer peer : peers) {
|
for (Peer peer : peers) {
|
||||||
@ -1022,14 +1021,21 @@ public class I2PSnarkServlet extends Default {
|
|||||||
out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
|
out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
|
||||||
out.write("</td>\n\t");
|
out.write("</td>\n\t");
|
||||||
out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
|
out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
|
||||||
float pct = (float) (100.0 * (float) peer.completed() / snark.meta.getPieces());
|
float pct;
|
||||||
if (pct == 100.0)
|
if (isValid) {
|
||||||
out.write(_("Seed"));
|
pct = (float) (100.0 * (float) peer.completed() / meta.getPieces());
|
||||||
else {
|
if (pct == 100.0)
|
||||||
String ps = String.valueOf(pct);
|
out.write(_("Seed"));
|
||||||
if (ps.length() > 5)
|
else {
|
||||||
ps = ps.substring(0, 5);
|
String ps = String.valueOf(pct);
|
||||||
out.write(ps + "%");
|
if (ps.length() > 5)
|
||||||
|
ps = ps.substring(0, 5);
|
||||||
|
out.write(ps + "%");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pct = (float) 101.0;
|
||||||
|
// until we get the metainfo we don't know how many pieces there are
|
||||||
|
out.write("??");
|
||||||
}
|
}
|
||||||
out.write("</td>\n\t");
|
out.write("</td>\n\t");
|
||||||
out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
|
out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
|
||||||
@ -1051,7 +1057,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
}
|
}
|
||||||
out.write("</td>\n\t");
|
out.write("</td>\n\t");
|
||||||
out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
|
out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
|
||||||
if (pct != 100.0) {
|
if (isValid && pct < 100.0) {
|
||||||
if (peer.isInterested() && !peer.isChoking()) {
|
if (peer.isInterested() && !peer.isChoking()) {
|
||||||
out.write("<span class=\"unchoked\">");
|
out.write("<span class=\"unchoked\">");
|
||||||
out.write(formatSize(peer.getUploadRate()) + "ps</span>");
|
out.write(formatSize(peer.getUploadRate()) + "ps</span>");
|
||||||
@ -1094,6 +1100,39 @@ public class I2PSnarkServlet extends Default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string or null
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
private String getTrackerLink(String announce, byte[] infohash) {
|
||||||
|
// temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash
|
||||||
|
if (announce != null && (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") ||
|
||||||
|
announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/") ||
|
||||||
|
announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/"))) {
|
||||||
|
Map<String, String> trackers = _manager.getTrackers();
|
||||||
|
for (Map.Entry<String, String> entry : trackers.entrySet()) {
|
||||||
|
String baseURL = entry.getValue();
|
||||||
|
if (!(baseURL.startsWith(announce) || // vvv hack for non-b64 announce in list vvv
|
||||||
|
(announce.startsWith("http://lnQ6yoBT") && baseURL.startsWith("http://tracker2.postman.i2p/")) ||
|
||||||
|
(announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/") && baseURL.startsWith("http://tracker2.postman.i2p/"))))
|
||||||
|
continue;
|
||||||
|
int e = baseURL.indexOf('=');
|
||||||
|
if (e < 0)
|
||||||
|
continue;
|
||||||
|
baseURL = baseURL.substring(e + 1);
|
||||||
|
String name = entry.getKey();
|
||||||
|
StringBuilder buf = new StringBuilder(128);
|
||||||
|
buf.append("<a href=\"").append(baseURL).append("details.php?dllist=1&filelist=1&info_hash=")
|
||||||
|
.append(TrackerClient.urlencode(infohash))
|
||||||
|
.append("\" title=\"").append(_("Details at {0} tracker", name)).append("\" target=\"_blank\">" +
|
||||||
|
"<img alt=\"").append(_("Info")).append("\" border=\"0\" src=\"")
|
||||||
|
.append(_imgPath).append("details.png\"></a>");
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
|
private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
|
||||||
String uri = req.getRequestURI();
|
String uri = req.getRequestURI();
|
||||||
String newURL = req.getParameter("newURL");
|
String newURL = req.getParameter("newURL");
|
||||||
@ -1167,6 +1206,10 @@ public class I2PSnarkServlet extends Default {
|
|||||||
out.write(":<td><select name=\"announceURL\"><option value=\"\">");
|
out.write(":<td><select name=\"announceURL\"><option value=\"\">");
|
||||||
out.write(_("Select a tracker"));
|
out.write(_("Select a tracker"));
|
||||||
out.write("</option>\n");
|
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();
|
Map trackers = _manager.getTrackers();
|
||||||
for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
|
for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
|
||||||
Map.Entry entry = (Map.Entry)iter.next();
|
Map.Entry entry = (Map.Entry)iter.next();
|
||||||
@ -1175,6 +1218,8 @@ public class I2PSnarkServlet extends Default {
|
|||||||
int e = announceURL.indexOf('=');
|
int e = announceURL.indexOf('=');
|
||||||
if (e > 0)
|
if (e > 0)
|
||||||
announceURL = announceURL.substring(0, e);
|
announceURL = announceURL.substring(0, e);
|
||||||
|
if (announceURL.equals(_lastAnnounceURL))
|
||||||
|
announceURL += "\" selected=\"selected";
|
||||||
out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n");
|
out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n");
|
||||||
}
|
}
|
||||||
out.write("</select>\n");
|
out.write("</select>\n");
|
||||||
@ -1308,15 +1353,17 @@ public class I2PSnarkServlet extends Default {
|
|||||||
out.write(" ");
|
out.write(" ");
|
||||||
out.write(renderOptions(0, 4, options.remove("outbound.length"), "outbound.length", HOP));
|
out.write(renderOptions(0, 4, options.remove("outbound.length"), "outbound.length", HOP));
|
||||||
|
|
||||||
out.write("<tr><td>");
|
if (!_context.isRouterContext()) {
|
||||||
out.write(_("I2CP host"));
|
out.write("<tr><td>");
|
||||||
out.write(": <td><input type=\"text\" name=\"i2cpHost\" value=\""
|
out.write(_("I2CP host"));
|
||||||
+ _manager.util().getI2CPHost() + "\" size=\"15\" > ");
|
out.write(": <td><input type=\"text\" name=\"i2cpHost\" value=\""
|
||||||
|
+ _manager.util().getI2CPHost() + "\" size=\"15\" > ");
|
||||||
|
|
||||||
out.write("<tr><td>");
|
out.write("<tr><td>");
|
||||||
out.write(_("I2CP port"));
|
out.write(_("I2CP port"));
|
||||||
out.write(": <td><input type=\"text\" name=\"i2cpPort\" class=\"r\" value=\"" +
|
out.write(": <td><input type=\"text\" name=\"i2cpPort\" class=\"r\" value=\"" +
|
||||||
+ _manager.util().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" > <br>\n");
|
+ _manager.util().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" > <br>\n");
|
||||||
|
}
|
||||||
|
|
||||||
StringBuilder opts = new StringBuilder(64);
|
StringBuilder opts = new StringBuilder(64);
|
||||||
for (Iterator iter = options.entrySet().iterator(); iter.hasNext(); ) {
|
for (Iterator iter = options.entrySet().iterator(); iter.hasNext(); ) {
|
||||||
@ -1344,6 +1391,49 @@ public class I2PSnarkServlet extends Default {
|
|||||||
out.write("</a></span></span></div>\n");
|
out.write("</a></span></span></div>\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param url in base32 or hex, xt must be first magnet param
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
private void addMagnet(String url) {
|
||||||
|
String ihash;
|
||||||
|
String name;
|
||||||
|
if (url.startsWith(MAGNET)) {
|
||||||
|
ihash = url.substring(MAGNET.length()).trim();
|
||||||
|
int amp = ihash.indexOf('&');
|
||||||
|
if (amp >= 0)
|
||||||
|
ihash = url.substring(0, amp);
|
||||||
|
name = "Magnet " + ihash;
|
||||||
|
} else if (url.startsWith(MAGGOT)) {
|
||||||
|
ihash = url.substring(MAGGOT.length()).trim();
|
||||||
|
int col = ihash.indexOf(':');
|
||||||
|
if (col >= 0)
|
||||||
|
ihash = url.substring(0, col);
|
||||||
|
name = "Maggot " + ihash;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byte[] ih = null;
|
||||||
|
if (ihash.length() == 32) {
|
||||||
|
ih = Base32.decode(ihash);
|
||||||
|
} else if (ihash.length() == 40) {
|
||||||
|
// Like DataHelper.fromHexString() but ensures no loss of leading zero bytes
|
||||||
|
ih = new byte[20];
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
ih[i] = (byte) (Integer.parseInt(ihash.substring(i*2, (i*2) + 2), 16) & 0xff);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
ih = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ih == null || ih.length != 20) {
|
||||||
|
_manager.addMessage(_("Invalid info hash in magnet URL {0}", url));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_manager.addMagnet(name, ih, true);
|
||||||
|
}
|
||||||
|
|
||||||
/** copied from ConfigTunnelsHelper */
|
/** copied from ConfigTunnelsHelper */
|
||||||
private static final String HOP = "hop";
|
private static final String HOP = "hop";
|
||||||
private static final String TUNNEL = "tunnel";
|
private static final String TUNNEL = "tunnel";
|
||||||
@ -1384,6 +1474,11 @@ public class I2PSnarkServlet extends Default {
|
|||||||
return _manager.util().getString(s, o);
|
return _manager.util().getString(s, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** translate */
|
||||||
|
private String _(String s, Object o, Object o2) {
|
||||||
|
return _manager.util().getString(s, o, o2);
|
||||||
|
}
|
||||||
|
|
||||||
/** translate (ngettext) @since 0.7.14 */
|
/** translate (ngettext) @since 0.7.14 */
|
||||||
private String ngettext(String s, String p, int n) {
|
private String ngettext(String s, String p, int n) {
|
||||||
return _manager.util().getString(n, s, p);
|
return _manager.util().getString(n, s, p);
|
||||||
@ -1459,13 +1554,11 @@ public class I2PSnarkServlet extends Default {
|
|||||||
private String getListHTML(Resource r, String base, boolean parent, Map postParams)
|
private String getListHTML(Resource r, String base, boolean parent, Map postParams)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
if (!r.isDirectory())
|
String[] ls = null;
|
||||||
return null;
|
if (r.isDirectory()) {
|
||||||
|
ls = r.list();
|
||||||
String[] ls = r.list();
|
Arrays.sort(ls, Collator.getInstance());
|
||||||
if (ls==null)
|
} // if r is not a directory, we are only showing torrent info section
|
||||||
return null;
|
|
||||||
Arrays.sort(ls, Collator.getInstance());
|
|
||||||
|
|
||||||
StringBuilder buf=new StringBuilder(4096);
|
StringBuilder buf=new StringBuilder(4096);
|
||||||
buf.append(DOCTYPE + "<HTML><HEAD><TITLE>");
|
buf.append(DOCTYPE + "<HTML><HEAD><TITLE>");
|
||||||
@ -1487,6 +1580,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
|
|
||||||
if (title.endsWith("/"))
|
if (title.endsWith("/"))
|
||||||
title = title.substring(0, title.length() - 1);
|
title = title.substring(0, title.length() - 1);
|
||||||
|
String directory = title;
|
||||||
title = _("Torrent") + ": " + title;
|
title = _("Torrent") + ": " + title;
|
||||||
buf.append(title);
|
buf.append(title);
|
||||||
buf.append("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" +
|
buf.append("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" +
|
||||||
@ -1495,13 +1589,68 @@ public class I2PSnarkServlet extends Default {
|
|||||||
|
|
||||||
if (parent) // always true
|
if (parent) // always true
|
||||||
buf.append("<div class=\"page\"><div class=\"mainsection\">");
|
buf.append("<div class=\"page\"><div class=\"mainsection\">");
|
||||||
boolean showPriority = snark != null && !snark.storage.complete();
|
boolean showPriority = ls != null && snark != null && snark.getStorage() != null && !snark.getStorage().complete();
|
||||||
if (showPriority)
|
if (showPriority)
|
||||||
buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
|
buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
|
||||||
buf.append("<TABLE BORDER=0 class=\"snarkTorrents\" >" +
|
buf.append("<TABLE BORDER=0 class=\"snarkTorrents\" ><thead>");
|
||||||
"<thead><tr><th>")
|
if (snark != null) {
|
||||||
|
// first row - torrent info
|
||||||
|
// FIXME center
|
||||||
|
buf.append("<tr><th colspan=\"" + (showPriority ? '4' : '3') + "\"><div>")
|
||||||
|
.append(_("Torrent")).append(": ").append(snark.getBaseName());
|
||||||
|
int pieces = snark.getPieces();
|
||||||
|
double completion = (pieces - snark.getNeeded()) / (double) pieces;
|
||||||
|
if (completion < 1.0)
|
||||||
|
buf.append("<br>").append(_("Completion")).append(": ").append((new DecimalFormat("0.00%")).format(completion));
|
||||||
|
else
|
||||||
|
buf.append("<br>").append(_("Complete"));
|
||||||
|
// else unknown
|
||||||
|
buf.append("<br>").append(_("Size")).append(": ").append(formatSize(snark.getTotalLength()));
|
||||||
|
MetaInfo meta = snark.getMetaInfo();
|
||||||
|
if (meta != null) {
|
||||||
|
List files = meta.getFiles();
|
||||||
|
int fileCount = files != null ? files.size() : 1;
|
||||||
|
buf.append("<br>").append(_("Files")).append(": ").append(fileCount);
|
||||||
|
}
|
||||||
|
buf.append("<br>").append(_("Pieces")).append(": ").append(pieces);
|
||||||
|
buf.append("<br>").append(_("Piece size")).append(": ").append(formatSize(snark.getPieceLength(0)));
|
||||||
|
|
||||||
|
if (meta != null) {
|
||||||
|
String announce = meta.getAnnounce();
|
||||||
|
if (announce != null) {
|
||||||
|
buf.append("<br>");
|
||||||
|
String trackerLink = getTrackerLink(announce, snark.getInfoHash());
|
||||||
|
if (trackerLink != null)
|
||||||
|
buf.append(trackerLink).append(' ');
|
||||||
|
buf.append(_("Tracker")).append(": ");
|
||||||
|
if (announce.startsWith("http://"))
|
||||||
|
announce = announce.substring(7);
|
||||||
|
int slsh = announce.indexOf('/');
|
||||||
|
if (slsh > 0)
|
||||||
|
announce = announce.substring(0, slsh);
|
||||||
|
buf.append(announce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String hex = I2PSnarkUtil.toHex(snark.getInfoHash());
|
||||||
|
buf.append("<br>").append(toImg("magnet", _("Magnet link"))).append(" <a href=\"")
|
||||||
|
.append(MAGNET).append(hex).append("\">")
|
||||||
|
.append(MAGNET).append(hex).append("</a>");
|
||||||
|
// We don't have the hash of the torrent file
|
||||||
|
//buf.append("<br>").append(_("Maggot link")).append(": <a href=\"").append(MAGGOT).append(hex).append(':').append(hex).append("\">")
|
||||||
|
// .append(MAGGOT).append(hex).append(':').append(hex).append("</a>");
|
||||||
|
buf.append("</div></th></tr>");
|
||||||
|
}
|
||||||
|
if (ls == null) {
|
||||||
|
// We are only showing the torrent info section
|
||||||
|
buf.append("</thead></table></div></div></BODY></HTML>");
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// second row - dir info
|
||||||
|
buf.append("<tr><th>")
|
||||||
.append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "file.png\" > ")
|
.append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "file.png\" > ")
|
||||||
.append(title).append("</th><th align=\"right\">")
|
.append(_("Directory")).append(": ").append(directory).append("</th><th align=\"right\">")
|
||||||
.append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "size.png\" > ")
|
.append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "size.png\" > ")
|
||||||
.append(_("Size"));
|
.append(_("Size"));
|
||||||
buf.append("</th><th class=\"headerstatus\">")
|
buf.append("</th><th class=\"headerstatus\">")
|
||||||
@ -1542,15 +1691,16 @@ public class I2PSnarkServlet extends Default {
|
|||||||
complete = true;
|
complete = true;
|
||||||
status = toImg("tick") + ' ' + _("Directory");
|
status = toImg("tick") + ' ' + _("Directory");
|
||||||
} else {
|
} else {
|
||||||
if (snark == null) {
|
if (snark == null || snark.getStorage() == null) {
|
||||||
// Assume complete, perhaps he removed a completed torrent but kept a bookmark
|
// Assume complete, perhaps he removed a completed torrent but kept a bookmark
|
||||||
complete = true;
|
complete = true;
|
||||||
status = toImg("cancel") + ' ' + _("Torrent not found?");
|
status = toImg("cancel") + ' ' + _("Torrent not found?");
|
||||||
} else {
|
} else {
|
||||||
|
Storage storage = snark.getStorage();
|
||||||
try {
|
try {
|
||||||
File f = item.getFile();
|
File f = item.getFile();
|
||||||
if (f != null) {
|
if (f != null) {
|
||||||
long remaining = snark.storage.remaining(f.getCanonicalPath());
|
long remaining = storage.remaining(f.getCanonicalPath());
|
||||||
if (remaining < 0) {
|
if (remaining < 0) {
|
||||||
complete = true;
|
complete = true;
|
||||||
status = toImg("cancel") + ' ' + _("File not found in torrent?");
|
status = toImg("cancel") + ' ' + _("File not found in torrent?");
|
||||||
@ -1558,7 +1708,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
complete = true;
|
complete = true;
|
||||||
status = toImg("tick") + ' ' + _("Complete");
|
status = toImg("tick") + ' ' + _("Complete");
|
||||||
} else {
|
} else {
|
||||||
int priority = snark.storage.getPriority(f.getCanonicalPath());
|
int priority = storage.getPriority(f.getCanonicalPath());
|
||||||
if (priority < 0)
|
if (priority < 0)
|
||||||
status = toImg("cancel");
|
status = toImg("cancel");
|
||||||
else if (priority == 0)
|
else if (priority == 0)
|
||||||
@ -1614,7 +1764,7 @@ public class I2PSnarkServlet extends Default {
|
|||||||
buf.append("<td class=\"priority\">");
|
buf.append("<td class=\"priority\">");
|
||||||
File f = item.getFile();
|
File f = item.getFile();
|
||||||
if ((!complete) && (!item.isDirectory()) && f != null) {
|
if ((!complete) && (!item.isDirectory()) && f != null) {
|
||||||
int pri = snark.storage.getPriority(f.getCanonicalPath());
|
int pri = snark.getStorage().getPriority(f.getCanonicalPath());
|
||||||
buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
|
buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
|
||||||
if (pri > 0)
|
if (pri > 0)
|
||||||
buf.append("checked=\"true\"");
|
buf.append("checked=\"true\"");
|
||||||
@ -1716,6 +1866,9 @@ public class I2PSnarkServlet extends Default {
|
|||||||
|
|
||||||
/** @since 0.8.1 */
|
/** @since 0.8.1 */
|
||||||
private void savePriorities(Snark snark, Map postParams) {
|
private void savePriorities(Snark snark, Map postParams) {
|
||||||
|
Storage storage = snark.getStorage();
|
||||||
|
if (storage == null)
|
||||||
|
return;
|
||||||
Set<Map.Entry> entries = postParams.entrySet();
|
Set<Map.Entry> entries = postParams.entrySet();
|
||||||
for (Map.Entry entry : entries) {
|
for (Map.Entry entry : entries) {
|
||||||
String key = (String)entry.getKey();
|
String key = (String)entry.getKey();
|
||||||
@ -1724,14 +1877,13 @@ public class I2PSnarkServlet extends Default {
|
|||||||
String file = key.substring(4);
|
String file = key.substring(4);
|
||||||
String val = ((String[])entry.getValue())[0]; // jetty arrays
|
String val = ((String[])entry.getValue())[0]; // jetty arrays
|
||||||
int pri = Integer.parseInt(val);
|
int pri = Integer.parseInt(val);
|
||||||
snark.storage.setPriority(file, pri);
|
storage.setPriority(file, pri);
|
||||||
//System.err.println("Priority now " + pri + " for " + file);
|
//System.err.println("Priority now " + pri + " for " + file);
|
||||||
} catch (Throwable t) { t.printStackTrace(); }
|
} catch (Throwable t) { t.printStackTrace(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (snark.coordinator != null)
|
snark.updatePiecePriorities();
|
||||||
snark.coordinator.updatePiecePriorities();
|
_manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities());
|
||||||
_manager.saveTorrentStatus(snark.storage.getMetaInfo(), snark.storage.getBitField(), snark.storage.getFilePriorities());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1753,15 +1905,18 @@ private static class FetchAndAdd implements Runnable {
|
|||||||
FileInputStream in = null;
|
FileInputStream in = null;
|
||||||
try {
|
try {
|
||||||
in = new FileInputStream(file);
|
in = new FileInputStream(file);
|
||||||
|
// we do not retain this MetaInfo object, hopefully it will go away quickly
|
||||||
MetaInfo info = new MetaInfo(in);
|
MetaInfo info = new MetaInfo(in);
|
||||||
String name = info.getName();
|
try { in.close(); } catch (IOException ioe) {}
|
||||||
name = DataHelper.stripHTML(name); // XSS
|
Snark snark = _manager.getTorrentByInfoHash(info.getInfoHash());
|
||||||
name = name.replace('/', '_');
|
if (snark != null) {
|
||||||
name = name.replace('\\', '_');
|
_manager.addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName()));
|
||||||
name = name.replace('&', '+');
|
return;
|
||||||
name = name.replace('\'', '_');
|
}
|
||||||
name = name.replace('"', '_');
|
|
||||||
name = name.replace('`', '_');
|
// don't hold object from this MetaInfo
|
||||||
|
String name = new String(info.getName());
|
||||||
|
name = Storage.filterName(name);
|
||||||
name = name + ".torrent";
|
name = name + ".torrent";
|
||||||
File torrentFile = new File(_manager.getDataDir(), name);
|
File torrentFile = new File(_manager.getDataDir(), name);
|
||||||
|
|
||||||
@ -1773,18 +1928,15 @@ private static class FetchAndAdd implements Runnable {
|
|||||||
else
|
else
|
||||||
_manager.addMessage(_("Torrent already in the queue: {0}", name));
|
_manager.addMessage(_("Torrent already in the queue: {0}", name));
|
||||||
} else {
|
} else {
|
||||||
boolean success = FileUtil.copy(file.getAbsolutePath(), canonical, false);
|
// This may take a LONG time to create the storage.
|
||||||
if (success) {
|
_manager.copyAndAddTorrent(file, canonical);
|
||||||
SecureFileOutputStream.setPerms(torrentFile);
|
|
||||||
_manager.addTorrent(canonical);
|
|
||||||
} else {
|
|
||||||
_manager.addMessage(_("Failed to copy torrent file to {0}", canonical));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
_manager.addMessage(_("Torrent at {0} was not valid", urlify(_url)) + ": " + ioe.getMessage());
|
_manager.addMessage(_("Torrent at {0} was not valid", urlify(_url)) + ": " + ioe.getMessage());
|
||||||
|
} catch (OutOfMemoryError oom) {
|
||||||
|
_manager.addMessage(_("ERROR - Out of memory, cannot create torrent from {0}", urlify(_url)) + ": " + oom.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
try { in.close(); } catch (IOException ioe) {}
|
try { if (in != null) in.close(); } catch (IOException ioe) {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_manager.addMessage(_("Torrent was not retrieved from {0}", urlify(_url)));
|
_manager.addMessage(_("Torrent was not retrieved from {0}", urlify(_url)));
|
||||||
|
Reference in New Issue
Block a user