diff --git a/INSTALL b/INSTALL index 068caf8bf..edb6f794e 100644 --- a/INSTALL +++ b/INSTALL @@ -9,9 +9,10 @@ tracker (an HTTP service), and a BitTorrent client. The only dependencies of the BitTorrent library are: * the log4j library +* the slf4j logging library * the SimpleHTTPFramework -These two libraries are provided in the lib/ directory, and are automatically +These libraries are provided in the lib/ directory, and are automatically included in the JAR file created by the build process. diff --git a/README b/README index cc2440ee2..fcefeeaeb 100644 --- a/README +++ b/README @@ -46,7 +46,8 @@ Authors * Thomas Zink - Fixed a piece length computation issue when the total torrent size is an exact multiple of the piece size. - +* Johan Parent + - Fixed a bug in unfresh peer collection Caveats ------- diff --git a/build.xml b/build.xml index 450a9abab..855726ba2 100644 --- a/build.xml +++ b/build.xml @@ -22,7 +22,7 @@ - + diff --git a/dist/ttorrent-1.1.1.jar b/dist/ttorrent-1.1.1.jar new file mode 100644 index 000000000..b3bbc95b9 Binary files /dev/null and b/dist/ttorrent-1.1.1.jar differ diff --git a/src/com/turn/ttorrent/client/Announce.java b/src/com/turn/ttorrent/client/Announce.java index 2a13bba9b..15ffdfc05 100644 --- a/src/com/turn/ttorrent/client/Announce.java +++ b/src/com/turn/ttorrent/client/Announce.java @@ -260,10 +260,10 @@ private Map announce(AnnounceEvent event, } params.put("peer_id", this.id); - params.put("port", new Integer(this.address.getPort()).toString()); - params.put("uploaded", new Long(this.torrent.getUploaded()).toString()); - params.put("downloaded", new Long(this.torrent.getDownloaded()).toString()); - params.put("left", new Long(this.torrent.getLeft()).toString()); + params.put("port", Integer.valueOf(this.address.getPort()).toString()); + params.put("uploaded", Long.valueOf(this.torrent.getUploaded()).toString()); + params.put("downloaded", Long.valueOf(this.torrent.getDownloaded()).toString()); + params.put("left", Long.valueOf(this.torrent.getLeft()).toString()); if (!AnnounceEvent.NONE.equals(event)) { params.put("event", event.name().toLowerCase()); diff --git a/src/com/turn/ttorrent/client/Client.java b/src/com/turn/ttorrent/client/Client.java index 1cee0e908..8e154877c 100644 --- a/src/com/turn/ttorrent/client/Client.java +++ b/src/com/turn/ttorrent/client/Client.java @@ -374,7 +374,7 @@ public synchronized void info() { } logger.info("BitTorrent client {}, {}/{}/{} peers, {}/{}/{} pieces " + - "({}%), {}/{} kB/s.", + "({}%, {} requested), {}/{} kB/s.", new Object[] { this.getState().name(), choked, @@ -384,6 +384,7 @@ public synchronized void info() { this.torrent.getAvailablePieces().cardinality(), this.torrent.getPieceCount(), String.format("%.2f", this.torrent.getCompletion()), + this.torrent.getRequestedPieces().cardinality(), String.format("%.2f", dl/1024.0), String.format("%.2f", ul/1024.0) }); @@ -437,14 +438,19 @@ private SharingPeer getOrCreatePeer(byte[] peerId, String ip, int port) { // Set peer ID for perviously known peer. peer.setPeerId(search.getPeerId()); + // Replace the mapping for this peer from its host + // identifier to its now known peer ID. this.peers.remove(peer.getHostIdentifier()); - this.peers.putIfAbsent(peer.getHexPeerId(), peer); + this.peers.put(peer.getHexPeerId(), peer); return peer; } } + // Last case, it really didn't exist already, add it, either from + // peer ID or host identifier, whatever we have so that we can find + // it later. peer = new SharingPeer(ip, port, search.getPeerId(), this.torrent); - this.peers.putIfAbsent(peer.hasPeerId() + this.peers.put(peer.hasPeerId() ? peer.getHexPeerId() : peer.getHostIdentifier(), peer); @@ -849,7 +855,7 @@ private synchronized void seed() { * * @author mpetazzoni */ - private class StopSeedingTask extends TimerTask { + private static class StopSeedingTask extends TimerTask { private Client client; diff --git a/src/com/turn/ttorrent/client/Handshake.java b/src/com/turn/ttorrent/client/Handshake.java index c51118aef..a9ed936ce 100644 --- a/src/com/turn/ttorrent/client/Handshake.java +++ b/src/com/turn/ttorrent/client/Handshake.java @@ -51,7 +51,7 @@ public byte[] getPeerId() { public static Handshake parse(ByteBuffer buffer) throws ParseException, UnsupportedEncodingException { - int pstrlen = new Byte(buffer.get()).intValue(); + int pstrlen = Byte.valueOf(buffer.get()).intValue(); if (pstrlen < 0 || buffer.remaining() != BASE_HANDSHAKE_LENGTH + pstrlen - 1) { throw new ParseException("Incorrect handshake message length " + diff --git a/src/com/turn/ttorrent/client/SharedTorrent.java b/src/com/turn/ttorrent/client/SharedTorrent.java index ba7154c38..9336763b6 100644 --- a/src/com/turn/ttorrent/client/SharedTorrent.java +++ b/src/com/turn/ttorrent/client/SharedTorrent.java @@ -28,6 +28,8 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; + +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; @@ -94,14 +96,13 @@ public class SharedTorrent extends Torrent implements PeerActivityListener { * @param torrent The Torrent object. * @param destDir The destination directory or location of the torrent * files. - * @throws IllegalArgumentException When the info dictionnary can't be - * encoded and hashed back to create the torrent's SHA-1 hash. * @throws FileNotFoundException If the torrent file location or * destination directory does not exist and can't be created. - * @throws IOException If the torrent file cannot be accessed. + * @throws IOException If the torrent file cannot be read or decoded. + * @throws NoSuchAlgorithmException */ public SharedTorrent(Torrent torrent, File destDir) - throws IllegalArgumentException, FileNotFoundException, IOException { + throws FileNotFoundException, IOException, NoSuchAlgorithmException { this(torrent, destDir, false); } @@ -115,14 +116,13 @@ public SharedTorrent(Torrent torrent, File destDir) * files. * @param seeder Whether we're a seeder for this torrent or not (disables * validation). - * @throws IllegalArgumentException When the info dictionnary can't be - * encoded and hashed back to create the torrent's SHA-1 hash. * @throws FileNotFoundException If the torrent file location or * destination directory does not exist and can't be created. - * @throws IOException If the torrent file cannot be accessed. + * @throws IOException If the torrent file cannot be read or decoded. + * @throws NoSuchAlgorithmException */ public SharedTorrent(Torrent torrent, File destDir, boolean seeder) - throws IllegalArgumentException, FileNotFoundException, IOException { + throws FileNotFoundException, IOException, NoSuchAlgorithmException { this(torrent.getEncoded(), destDir, seeder); } @@ -131,14 +131,12 @@ public SharedTorrent(Torrent torrent, File destDir, boolean seeder) * @param torrent The metainfo byte data. * @param destDir The destination directory or location of the torrent * files. - * @throws IllegalArgumentException When the info dictionary can't be - * encoded and hashed back to create the torrent's SHA-1 hash. * @throws FileNotFoundException If the torrent file location or * destination directory does not exist and can't be created. - * @throws IOException If the torrent file cannot be accessed. + * @throws IOException If the torrent file cannot be read or decoded. */ public SharedTorrent(byte[] torrent, File destDir) - throws IllegalArgumentException, FileNotFoundException, IOException { + throws FileNotFoundException, IOException, NoSuchAlgorithmException { this(torrent, destDir, false); } @@ -148,14 +146,13 @@ public SharedTorrent(byte[] torrent, File destDir) * @param parent The parent directory or location the torrent files. * @param seeder Whether we're a seeder for this torrent or not (disables * validation). - * @throws IllegalArgumentException When the info dictionary can't be - * encoded and hashed back to create the torrent's SHA-1 hash. * @throws FileNotFoundException If the torrent file location or * destination directory does not exist and can't be created. - * @throws IOException If the torrent file cannot be accessed. + * @throws IOException If the torrent file cannot be read or decoded. + * @throws NoSuchAlgorithmException */ public SharedTorrent(byte[] torrent, File parent, boolean seeder) - throws IllegalArgumentException, FileNotFoundException, IOException { + throws FileNotFoundException, IOException, NoSuchAlgorithmException { super(torrent, parent, seeder); if (parent == null || !parent.isDirectory()) { @@ -214,12 +211,11 @@ public SharedTorrent(byte[] torrent, File parent, boolean seeder) * @param source The .torrent file to read the torrent * meta-info from. * @param parent The parent directory or location of the torrent files. - * @throws IllegalArgumentException When the info dictionnary can't be - * encoded and hashed back to create the torrent's SHA-1 hash. - * @throws IOException When the torrent file cannot be read. + * @throws IOException When the torrent file cannot be read or decoded. + * @throws NoSuchAlgorithmException */ public static SharedTorrent fromFile(File source, File parent) - throws IllegalArgumentException, IOException { + throws IOException, NoSuchAlgorithmException { FileInputStream fis = new FileInputStream(source); byte[] data = new byte[(int)source.length()]; fis.read(data); @@ -278,8 +274,8 @@ public synchronized void init() throws InterruptedException, IOException { throw new IllegalStateException("Torrent was already initialized!"); } - int nPieces = new Double(Math.ceil((double)this.getSize() / - this.pieceLength)).intValue(); + int nPieces = (int) (Math.ceil( + (double)this.getSize() / this.pieceLength)); this.pieces = new Piece[nPieces]; this.completedPieces = new BitSet(nPieces); @@ -404,6 +400,8 @@ public BitSet getAvailablePieces() { return availablePieces; } + /** Return a copy of the completed pieces bitset. + */ public BitSet getCompletedPieces() { if (!this.isInitialized()) { throw new IllegalStateException("Torrent not yet initialized!"); @@ -414,6 +412,18 @@ public BitSet getCompletedPieces() { } } + /** Return a copy of the requested pieces bitset. + */ + public BitSet getRequestedPieces() { + if (!this.isInitialized()) { + throw new IllegalStateException("Torrent not yet initialized!"); + } + + synchronized (this.requestedPieces) { + return (BitSet)this.requestedPieces.clone(); + } + } + /** Tells whether this torrent has been fully downloaded, or is fully * available locally. */ diff --git a/src/com/turn/ttorrent/client/peer/SharingPeer.java b/src/com/turn/ttorrent/client/peer/SharingPeer.java index 68d0106c0..e936949b7 100644 --- a/src/com/turn/ttorrent/client/peer/SharingPeer.java +++ b/src/com/turn/ttorrent/client/peer/SharingPeer.java @@ -323,7 +323,7 @@ public synchronized void downloadPiece(Piece piece) IllegalStateException up = new IllegalStateException( "Trying to download a piece while previous " + "download not completed!"); - logger.warn("{}", up); + logger.warn("What's going on? {}", up.getMessage(), up); throw up; // ah ah. } @@ -403,6 +403,8 @@ private synchronized Set cancelPendingRequests() { request.getOffset(), request.getLength())); requests.add(request); } + + this.requests = null; } return requests; diff --git a/src/com/turn/ttorrent/common/Torrent.java b/src/com/turn/ttorrent/common/Torrent.java index 92ebf05ba..f858b7865 100644 --- a/src/com/turn/ttorrent/common/Torrent.java +++ b/src/com/turn/ttorrent/common/Torrent.java @@ -122,67 +122,61 @@ public TorrentFile(File file, long size) { * @param torrent The metainfo byte data. * @param parent The parent directory or location of the torrent files. * @param seeder Whether we'll be seeding for this torrent or not. - * @throws IllegalArgumentException When the info dictionnary can't be + * @throws IOException When the info dictionnary can't be read or * encoded and hashed back to create the torrent's SHA-1 hash. + * @throws NoSuchAlgorithmException If the SHA-1 algorithm is not + * available. */ public Torrent(byte[] torrent, File parent, boolean seeder) - throws IllegalArgumentException { + throws IOException, NoSuchAlgorithmException { this.encoded = torrent; this.seeder = seeder; - try { - this.decoded = BDecoder.bdecode( - new ByteArrayInputStream(this.encoded)).getMap(); - - this.decoded_info = this.decoded.get("info").getMap(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - BEncoder.bencode(this.decoded_info, baos); - this.encoded_info = baos.toByteArray(); - this.info_hash = Torrent.hash(this.encoded_info); - this.hex_info_hash = Torrent.byteArrayToHexString(this.info_hash); - - this.announceUrl = this.decoded.get("announce").getString(); - this.createdBy = this.decoded.containsKey("created by") - ? this.decoded.get("created by").getString() - : null; - this.name = this.decoded_info.get("name").getString(); - - this.files = new LinkedList(); - - // Parse multi-file torrent file information structure. - if (this.decoded_info.containsKey("files")) { - // For multi-file torrents, the name of the torrent serves as - // the top-level parent directory. - parent = new File(parent, this.name); - - for (BEValue file : this.decoded_info.get("files").getList()) { - Map fileInfo = file.getMap(); - StringBuffer path = new StringBuffer(); - for (BEValue pathElement : fileInfo.get("path").getList()) { - path.append(File.separator) - .append(pathElement.getString()); - } - this.files.add(new TorrentFile( - new File(this.name, path.toString()), - fileInfo.get("length").getLong())); + this.decoded = BDecoder.bdecode( + new ByteArrayInputStream(this.encoded)).getMap(); + + this.decoded_info = this.decoded.get("info").getMap(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BEncoder.bencode(this.decoded_info, baos); + this.encoded_info = baos.toByteArray(); + this.info_hash = Torrent.hash(this.encoded_info); + this.hex_info_hash = Torrent.byteArrayToHexString(this.info_hash); + + this.announceUrl = this.decoded.get("announce").getString(); + this.createdBy = this.decoded.containsKey("created by") + ? this.decoded.get("created by").getString() + : null; + this.name = this.decoded_info.get("name").getString(); + + this.files = new LinkedList(); + + // Parse multi-file torrent file information structure. + if (this.decoded_info.containsKey("files")) { + for (BEValue file : this.decoded_info.get("files").getList()) { + Map fileInfo = file.getMap(); + StringBuffer path = new StringBuffer(); + for (BEValue pathElement : fileInfo.get("path").getList()) { + path.append(File.separator) + .append(pathElement.getString()); } - } else { - // For single-file torrents, the name of the torrent is - // directly the name of the file. this.files.add(new TorrentFile( - new File(this.name), - this.decoded_info.get("length").getLong())); + new File(this.name, path.toString()), + fileInfo.get("length").getLong())); } + } else { + // For single-file torrents, the name of the torrent is + // directly the name of the file. + this.files.add(new TorrentFile( + new File(this.name), + this.decoded_info.get("length").getLong())); + } - // Calculate the total size of this torrent from its files' sizes. - long size = 0; - for (TorrentFile file : this.files) { - size += file.size; - } - this.size = size; - } catch (Exception e) { - throw new IllegalArgumentException("Can't parse torrent information!", e); + // Calculate the total size of this torrent from its files' sizes. + long size = 0; + for (TorrentFile file : this.files) { + size += file.size; } + this.size = size; logger.info("{}-file torrent information:", this.isMultifile() ? "Multi" : "Single"); @@ -294,10 +288,16 @@ public boolean isSeeder() { * @throws IOException If an I/O error occurs while writing the file. */ public void save(File output) throws IOException { - FileOutputStream fos = new FileOutputStream(output); - fos.write(this.getEncoded()); - fos.close(); - logger.info("Wrote torrent file {}.", output.getAbsolutePath()); + FileOutputStream fos = null; + try { + fos = new FileOutputStream(output); + fos.write(this.getEncoded()); + logger.info("Wrote torrent file {}.", output.getAbsolutePath()); + } finally { + if (fos != null) { + fos.close(); + } + } } public static byte[] hash(byte[] data) throws NoSuchAlgorithmException { @@ -370,8 +370,10 @@ protected static int getHashingThreadsCount() { * .torrent file to load. * @param parent * @throws IOException When the torrent file cannot be read. + * @throws NoSuchAlgorithmException */ - public static Torrent load(File torrent, File parent) throws IOException { + public static Torrent load(File torrent, File parent) + throws IOException, NoSuchAlgorithmException { return Torrent.load(torrent, parent, false); } @@ -383,14 +385,21 @@ public static Torrent load(File torrent, File parent) throws IOException { * @param seeder Whether we are a seeder for this torrent or not (disables * local data validation). * @throws IOException When the torrent file cannot be read. + * @throws NoSuchAlgorithmException */ public static Torrent load(File torrent, File parent, boolean seeder) - throws IOException { - FileInputStream fis = new FileInputStream(torrent); - byte[] data = new byte[(int)torrent.length()]; - fis.read(data); - fis.close(); - return new Torrent(data, parent, seeder); + throws IOException, NoSuchAlgorithmException { + FileInputStream fis = null; + try { + fis = new FileInputStream(torrent); + byte[] data = new byte[(int)torrent.length()]; + fis.read(data); + return new Torrent(data, parent, seeder); + } finally { + if (fis != null) { + fis.close(); + } + } } /** Torrent creation --------------------------------------------------- */ @@ -584,8 +593,8 @@ private static String hashFiles(List files) throw new IOException("Error while hashing the torrent data!", ee); } - int expectedPieces = new Double(Math.ceil((double)length / - Torrent.PIECE_LENGTH)).intValue(); + int expectedPieces = (int) (Math.ceil( + (double)length / Torrent.PIECE_LENGTH)); logger.info("Hashed {} file(s) ({} bytes) in {} pieces ({} expected) in {}ms.", new Object[] { files.size(), diff --git a/src/com/turn/ttorrent/tracker/TrackedTorrent.java b/src/com/turn/ttorrent/tracker/TrackedTorrent.java index 93a561b64..2e8a79ada 100644 --- a/src/com/turn/ttorrent/tracker/TrackedTorrent.java +++ b/src/com/turn/ttorrent/tracker/TrackedTorrent.java @@ -18,8 +18,11 @@ import com.turn.ttorrent.bcodec.BEValue; import com.turn.ttorrent.common.Torrent; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; + +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -51,10 +54,13 @@ public class TrackedTorrent extends Torrent { /** Create a new tracked torrent from metainfo binary data. * * @param torrent The metainfo byte data. - * @throws IllegalArgumentException When the info dictionnary can't be + * @throws IOException When the info dictionnary can't be * encoded and hashed back to create the torrent's SHA-1 hash. + * @throws NoSuchAlgorithmException If the SHA-1 algorithm is not + * available. */ - public TrackedTorrent(byte[] torrent) throws IllegalArgumentException { + public TrackedTorrent(byte[] torrent) + throws IOException, NoSuchAlgorithmException { super(torrent, null, false); this.peers = new ConcurrentHashMap(); @@ -62,7 +68,8 @@ public TrackedTorrent(byte[] torrent) throws IllegalArgumentException { this.announceInterval = TrackedTorrent.DEFAULT_ANNOUNCE_INTERVAL_SECONDS; } - public TrackedTorrent(Torrent torrent) throws IllegalArgumentException { + public TrackedTorrent(Torrent torrent) + throws IOException, NoSuchAlgorithmException { this(torrent.getEncoded()); } @@ -129,7 +136,7 @@ private int leechers() { public void collectUnfreshPeers() { for (TrackedPeer peer : this.peers.values()) { if (!peer.isFresh()) { - this.peers.remove(peer); + this.peers.remove(peer.getHexPeerId()); } } } diff --git a/src/com/turn/ttorrent/tracker/Tracker.java b/src/com/turn/ttorrent/tracker/Tracker.java index 7dbef71c1..b12e69d99 100644 --- a/src/com/turn/ttorrent/tracker/Tracker.java +++ b/src/com/turn/ttorrent/tracker/Tracker.java @@ -22,6 +22,8 @@ import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.URL; + +import java.security.NoSuchAlgorithmException; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; @@ -158,14 +160,23 @@ public synchronized TrackedTorrent announce(Torrent newTorrent) { logger.warn("Tracker already announced torrent for '{}' " + "with hash {}.", torrent.getName(), torrent.getHexInfoHash()); return torrent; - } else { + } + + try { torrent = new TrackedTorrent(newTorrent); + this.torrents.put(torrent.getHexInfoHash(), torrent); + logger.info("Registered new torrent for '{}' with hash {}.", + torrent.getName(), torrent.getHexInfoHash()); + return torrent; + } catch (IOException ioe) { + logger.warn("Could not announce new torrent: " + + ioe.getMessage()); + } catch (NoSuchAlgorithmException nsae) { + logger.error("Could not announce new torrent: " + + nsae.getMessage()); } - this.torrents.put(torrent.getHexInfoHash(), torrent); - logger.info("Registered new torrent for '{}' with hash {}.", - torrent.getName(), torrent.getHexInfoHash()); - return torrent; + return null; } /** Stop announcing the given torrent. @@ -198,7 +209,7 @@ public synchronized void remove(Torrent torrent, long delay) { * This task can be used to stop announcing a torrent after a certain delay * through a Timer. */ - private class TorrentRemoveTimer extends TimerTask { + private static class TorrentRemoveTimer extends TimerTask { private Tracker tracker; private Torrent torrent;