diff --git a/build.xml b/build.xml index 6b768ab30..3e42c8cdc 100644 --- a/build.xml +++ b/build.xml @@ -22,7 +22,7 @@ - + diff --git a/dist/ttorrent-1.0.4.jar b/dist/ttorrent-1.0.4.jar new file mode 100644 index 000000000..ede0ebd57 Binary files /dev/null and b/dist/ttorrent-1.0.4.jar differ diff --git a/lib/commons-io-2.0.1.jar b/lib/commons-io-2.0.1.jar new file mode 100644 index 000000000..5b64b7d6c Binary files /dev/null and b/lib/commons-io-2.0.1.jar differ diff --git a/src/com/turn/ttorrent/client/Client.java b/src/com/turn/ttorrent/client/Client.java index 29fe8d503..014acd290 100644 --- a/src/com/turn/ttorrent/client/Client.java +++ b/src/com/turn/ttorrent/client/Client.java @@ -221,11 +221,28 @@ public synchronized void share(int seed) { /** Immediately but gracefully stop this client. */ - public synchronized void stop() { + public void stop() { + this.stop(true); + } + + /** Immediately but gracefully stop this client. + * + * @param wait Whether to wait for the client execution thread to complete + * or not. This allows for the client's state to be settled down in one of + * the DONE or ERROR states when this method returns. + */ + public void stop(boolean wait) { this.stop = true; if (this.thread != null && this.thread.isAlive()) { this.thread.interrupt(); + if (wait) { + try { + this.thread.join(); + } catch (InterruptedException ie) { + // Ignore + } + } } this.thread = null; @@ -316,6 +333,8 @@ public void run() { peer.unbind(true); } + this.torrent.close(); + // Determine final state if (this.torrent.isComplete()) { this.setState(ClientState.DONE); diff --git a/src/com/turn/ttorrent/client/SharedTorrent.java b/src/com/turn/ttorrent/client/SharedTorrent.java index af2ebb95c..14655f957 100644 --- a/src/com/turn/ttorrent/client/SharedTorrent.java +++ b/src/com/turn/ttorrent/client/SharedTorrent.java @@ -237,6 +237,15 @@ public synchronized void init() throws IOException { this.pieces.length + "]."); } + public synchronized void close() { + try { + this.bucket.close(); + } catch (IOException ioe) { + logger.error("Error closing torrent byte storage: " + + ioe.getMessage()); + } + } + /** Retrieve a piece object by index. * * @param index The index of the piece in this torrent. @@ -506,6 +515,15 @@ public synchronized void handlePieceCompleted(SharingPeer peer, Piece piece) { logger.warn("Downloaded piece " + piece + " was not valid ;-("); } + if (this.isComplete()) { + try { + this.bucket.complete(); + } catch (IOException ioe) { + logger.error("Could not move downloaded file(s) to their " + + "target location!", ioe); + } + } + logger.trace("We now have " + this.completedPieces.cardinality() + " piece(s) and " + this.requestedPieces.cardinality() + " outstanding request(s): " + this.requestedPieces + "."); diff --git a/src/com/turn/ttorrent/client/TorrentByteStorage.java b/src/com/turn/ttorrent/client/TorrentByteStorage.java index 02cdff408..d1c06ffde 100644 --- a/src/com/turn/ttorrent/client/TorrentByteStorage.java +++ b/src/com/turn/ttorrent/client/TorrentByteStorage.java @@ -21,21 +21,28 @@ import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; /** Torrent data storage. * + *

* A torrent, regardless of whether it contains multiple files or not, is * considered as one linear, contiguous byte array. As such, pieces can spread * across multiple files. + *

* + *

* Although this BitTorrent client currently only supports single-torrent * files, this TorrentByteStorage class provides an abstraction for the Piece * class to read and write to the torrent's data without having to care about * which file(s) a piece is on. + *

* + *

* The current implementation uses a RandomAccessFile FileChannel to expose * thread-safe read/write methods. + *

* * @author mpetazzoni */ @@ -44,29 +51,96 @@ public class TorrentByteStorage { private static final Logger logger = Logger.getLogger(TorrentByteStorage.class); + private static final String PARTIAL_FILE_NAME_SUFFIX = ".part"; + + private File target; + private File partial; + private File current; + + private RandomAccessFile raf; private FileChannel channel; + private long size; + + public TorrentByteStorage(File file, long size) throws IOException { + this.target = file; + this.size = size; + + this.partial = new File(this.target.getAbsolutePath() + + TorrentByteStorage.PARTIAL_FILE_NAME_SUFFIX); - public TorrentByteStorage(File file, int size) throws IOException { - RandomAccessFile raf = new RandomAccessFile(file, "rw"); + if (this.partial.exists()) { + logger.info("Partial download found at " + + this.partial.getAbsolutePath() + ". Continuing..."); + this.current = this.partial; + } else if (!this.target.exists()) { + logger.info("Downloading new file to " + + this.partial.getAbsolutePath() + "..."); + this.current = this.partial; + } else { + logger.info("Using existing file " + + this.target.getAbsolutePath() + "."); + this.current = this.target; + } + + this.raf = new RandomAccessFile(this.current, "rw"); // Set the file length to the appropriate size, eventually truncating // or extending the file if it already exists with a different size. - raf.setLength(size); + this.raf.setLength(this.size); this.channel = raf.getChannel(); logger.debug("Initialized torrent byte storage at " + - file.getAbsolutePath() + "."); + this.current.getAbsolutePath() + "."); } public ByteBuffer read(int offset, int length) throws IOException { ByteBuffer data = ByteBuffer.allocate(length); - int bytes = channel.read(data, offset); + int bytes = this.channel.read(data, offset); data.clear(); data.limit(bytes >= 0 ? bytes : 0); return data; } public void write(ByteBuffer block, int offset) throws IOException { - channel.write(block, offset); + this.channel.write(block, offset); + } + + /** Move the partial file to its final location. + * + *

+ * This method needs to make sure reads can still happen seemlessly during + * the operation. The partial is first flushed to the storage device before + * being copied to its target location. The {@link FileChannel} is then + * switched to this new file before the partial is removed. + *

+ */ + public synchronized void complete() throws IOException { + this.channel.force(true); + + // Nothing more to do if we're already on the target file. + if (this.current.equals(this.target)) { + return; + } + + FileUtils.deleteQuietly(this.target); + FileUtils.copyFile(this.current, this.target); + + logger.debug("Re-opening torrent byte storage at " + + this.target.getAbsolutePath() + "."); + + RandomAccessFile raf = new RandomAccessFile(this.target, "rw"); + raf.setLength(this.size); + + this.channel = raf.getChannel(); + this.raf.close(); + this.raf = raf; + this.current = this.target; + + FileUtils.deleteQuietly(this.partial); + } + + public synchronized void close() throws IOException { + this.channel.force(true); + this.raf.close(); } }