Skip to content

Commit

Permalink
Implement download to temporary file
Browse files Browse the repository at this point in the history
Instead of directly downloading into the target file, download to a
temporary file and move it to its final location/name when the download
is complete. This avoids leaving a partially downloaded file in its
final requested location and name.

Also provides a wait for waiting for the client to complete its
operation after stopping is requested.

Bump to version 1.0.4.

Signed-off-by: Maxime Petazzoni <mpetazzoni@turn.com>
  • Loading branch information
Maxime Petazzoni committed Jul 19, 2011
1 parent d852ed0 commit bcdd593
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 8 deletions.
2 changes: 1 addition & 1 deletion build.xml
Expand Up @@ -22,7 +22,7 @@
<property name="src.dir" location="src" />

<!-- Release version. -->
<property name="project.version" value="1.0.3" />
<property name="project.version" value="1.0.4" />

<path id="project.classpath">
<pathelement location="${build.dir}" />
Expand Down
Binary file added dist/ttorrent-1.0.4.jar
Binary file not shown.
Binary file added lib/commons-io-2.0.1.jar
Binary file not shown.
21 changes: 20 additions & 1 deletion src/com/turn/ttorrent/client/Client.java
Expand Up @@ -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 <tt>DONE</tt> or <tt>ERROR</tt> 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;
Expand Down Expand Up @@ -316,6 +333,8 @@ public void run() {
peer.unbind(true);
}

this.torrent.close();

// Determine final state
if (this.torrent.isComplete()) {
this.setState(ClientState.DONE);
Expand Down
18 changes: 18 additions & 0 deletions src/com/turn/ttorrent/client/SharedTorrent.java
Expand Up @@ -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.
Expand Down Expand Up @@ -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 + ".");
Expand Down
86 changes: 80 additions & 6 deletions src/com/turn/ttorrent/client/TorrentByteStorage.java
Expand Up @@ -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.
*
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* <p>
* The current implementation uses a RandomAccessFile FileChannel to expose
* thread-safe read/write methods.
* </p>
*
* @author mpetazzoni
*/
Expand All @@ -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.
*
* <p>
* 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.
* </p>
*/
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();
}
}

0 comments on commit bcdd593

Please sign in to comment.