* 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(); } }