package org.limewire.libtorrent;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.limewire.bittorrent.BTData;
import org.limewire.bittorrent.BTDataImpl;
import org.limewire.bittorrent.Torrent;
import org.limewire.bittorrent.TorrentAlert;
import org.limewire.bittorrent.TorrentEvent;
import org.limewire.bittorrent.TorrentFileEntry;
import org.limewire.bittorrent.TorrentInfo;
import org.limewire.bittorrent.TorrentManager;
import org.limewire.bittorrent.TorrentParams;
import org.limewire.bittorrent.TorrentPeer;
import org.limewire.bittorrent.TorrentStatus;
import org.limewire.bittorrent.bencoding.Token;
import org.limewire.io.IOUtils;
import org.limewire.listener.AsynchronousEventMulticaster;
import org.limewire.listener.AsynchronousMulticasterImpl;
import org.limewire.listener.EventListener;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.util.FileUtils;
import org.limewire.util.StringUtils;
import com.google.inject.Inject;
import com.google.inject.name.Named;
/**
* Class representing the torrent being downloaded. It is updated periodically
* by the TorrentManager. It has all necessary helper methods to provide
* functionality to the BTDownloaderImpl. It delegates calls to native methods
* back to the TorrentManager.
*/
public class TorrentImpl implements Torrent {
private static final Log LOG = LogFactory.getLog(TorrentImpl.class);
private final AsynchronousEventMulticaster<TorrentEvent> listeners;
private final TorrentManager torrentManager;
private final AtomicReference<TorrentStatus> status = new AtomicReference<TorrentStatus>(null);
private final AtomicReference<TorrentInfo> torrentInfo = new AtomicReference<TorrentInfo>(null);
private final AtomicReference<File> torrentDataFile = new AtomicReference<File>(null);
private final AtomicReference<File> torrentFile = new AtomicReference<File>(null);
private final AtomicReference<File> fastResumeFile = new AtomicReference<File>(null);
private String sha1 = null;
private String name = null;
private String trackerURL = null;
private final AtomicBoolean started = new AtomicBoolean(false);
private final AtomicBoolean cancelled = new AtomicBoolean(false);
// used to decide if the torrent was just newly completed or not.
private final AtomicBoolean complete = new AtomicBoolean(false);
private Boolean isPrivate = null;
@Inject
public TorrentImpl(TorrentManager torrentManager,
@Named("fastExecutor") ScheduledExecutorService fastExecutor) {
this.torrentManager = torrentManager;
listeners = new AsynchronousMulticasterImpl<TorrentEvent>(fastExecutor);
}
@Override
public void addListener(EventListener<TorrentEvent> listener) {
listeners.addListener(listener);
}
@Override
public boolean removeListener(EventListener<TorrentEvent> listener) {
return listeners.removeListener(listener);
}
@Override
public synchronized void init(TorrentParams params) throws IOException {
this.sha1 = params.getSha1();
this.trackerURL = params.getTrackerURL();
this.name = params.getName();
this.isPrivate = params.getPrivate();
File torrentFile = params.getTorrentFile();
File fastResumeFile = params.getFastResumeFile();
File torrentDataFile = params.getTorrentDataFile();
if (torrentFile != null && torrentFile.exists()) {
FileInputStream fis = null;
FileChannel fileChannel = null;
try {
fis = new FileInputStream(torrentFile);
fileChannel = fis.getChannel();
Map metaInfo = (Map) Token.parse(fileChannel);
BTData btData = new BTDataImpl(metaInfo);
if (this.name == null) {
this.name = btData.getName();
}
if (this.trackerURL == null) {
this.trackerURL = btData.getAnnounce();
}
if (this.sha1 == null) {
this.sha1 = StringUtils.toHexString(btData.getInfoHash());
}
if (this.isPrivate == null) {
this.isPrivate = btData.isPrivate();
}
} finally {
IOUtils.close(fileChannel);
IOUtils.close(fis);
}
}
if (this.isPrivate == null) {
// private by default if unknown
this.isPrivate = Boolean.TRUE;
}
File torrentDownloadFolder = torrentManager.getTorrentManagerSettings()
.getTorrentDownloadFolder();
if (this.name == null || torrentDownloadFolder == null || this.sha1 == null) {
throw new IOException("There was an error initializing the torrent.");
}
this.fastResumeFile.set(fastResumeFile == null ? new File(torrentDownloadFolder, this.name
+ ".fastresume") : fastResumeFile);
this.torrentDataFile.set(torrentDataFile == null ? new File(torrentDownloadFolder,
this.name) : torrentDataFile);
this.torrentFile.set(torrentFile == null ? new File(torrentDownloadFolder, this.name
+ ".torrent") : torrentFile);
}
@Override
public String getName() {
return name;
}
@Override
public void start() {
if (!started.getAndSet(true)) {
resume();
listeners.broadcast(TorrentEvent.STARTED);
}
}
@Override
public File getTorrentFile() {
return torrentFile.get();
}
@Override
public File getFastResumeFile() {
return fastResumeFile.get();
}
@Override
public synchronized void moveTorrent(File directory) {
// TODO potentially rename the method, or at least put in another
// parameter to use as the directory to move the torrent file and fast
// resume files to.
assert isFinished();
torrentManager.moveTorrent(this, directory);
torrentDataFile.set(new File(directory, torrentDataFile.get().getName()));
// TODO would be nice to move the following logic to something outside
// of
// the torrent code, since it is not really the torrent codes
// responsibility.
File oldFastResumeFile = fastResumeFile.get();
File oldTorrentFile = torrentFile.get();
fastResumeFile.set(new File(torrentManager.getTorrentManagerSettings()
.getTorrentUploadsFolder(), oldFastResumeFile.getName()));
torrentFile.set(new File(torrentManager.getTorrentManagerSettings()
.getTorrentUploadsFolder(), oldTorrentFile.getName()));
FileUtils.copy(oldTorrentFile, torrentFile.get());
FileUtils.copy(oldFastResumeFile, fastResumeFile.get());
FileUtils.forceDelete(oldTorrentFile);
FileUtils.forceDelete(oldFastResumeFile);
}
@Override
public void pause() {
torrentManager.pauseTorrent(this);
}
@Override
public void resume() {
if (getStatus().isError()) {
torrentManager.recoverTorrent(this);
} else {
torrentManager.resumeTorrent(this);
}
}
@Override
public float getDownloadRate() {
TorrentStatus status = this.status.get();
return status == null ? 0 : status.getDownloadPayloadRate();
}
@Override
public String getSha1() {
return sha1;
}
@Override
public boolean isPaused() {
TorrentStatus status = this.status.get();
return status == null ? false : status.isPaused();
}
@Override
public boolean isFinished() {
TorrentStatus status = this.status.get();
return status == null ? false : status.isFinished();
}
@Override
public boolean isStarted() {
return started.get();
}
@Override
public String getTrackerURL() {
return trackerURL;
}
@Override
public boolean isMultiFileTorrent() {
return getTorrentFileEntries().size() > 0;
}
@Override
public int getNumPeers() {
TorrentStatus status = this.status.get();
return status == null ? 0 : status.getNumPeers();
}
@Override
public File getTorrentDataFile() {
return torrentDataFile.get();
}
@Override
public boolean isSingleFileTorrent() {
return !isMultiFileTorrent();
}
@Override
public void stop() {
if (started.get() && !cancelled.getAndSet(true)) {
torrentManager.removeTorrent(this);
}
listeners.broadcast(TorrentEvent.STOPPED);
}
@Override
public long getTotalUploaded() {
TorrentStatus status = this.status.get();
if (status == null) {
return 0;
} else {
return status.getAllTimePayloadUpload();
}
}
@Override
public int getNumUploads() {
TorrentStatus status = this.status.get();
if (status == null) {
return 0;
} else {
return status.getNumUploads();
}
}
@Override
public float getUploadRate() {
TorrentStatus status = this.status.get();
return status == null ? 0 : status.getUploadPayloadRate();
}
@Override
public float getSeedRatio() {
TorrentStatus status = this.status.get();
if (status != null) {
float seedRatio = status.getSeedRatio();
return seedRatio;
}
return 0;
}
@Override
public boolean isCancelled() {
return cancelled.get();
}
@Override
public TorrentStatus getStatus() {
return status.get();
}
@Override
public void updateStatus(TorrentStatus torrentStatus) {
if (!cancelled.get()) {
synchronized (TorrentImpl.this) {
TorrentImpl.this.status.set(torrentStatus);
boolean newlyfinished = !complete.get() && torrentStatus.isFinished();
complete.set(torrentStatus.isFinished());
if (newlyfinished) {
listeners.broadcast(TorrentEvent.COMPLETED);
} else {
listeners.broadcast(TorrentEvent.STATUS_CHANGED);
}
}
}
}
@Override
public void alert(TorrentAlert alert) {
synchronized (TorrentImpl.this) {
if (alert.getCategory() == TorrentAlert.SAVE_RESUME_DATA_ALERT) {
listeners.broadcast(TorrentEvent.FAST_RESUME_FILE_SAVED);
}
}
}
@Override
public boolean registerWithTorrentManager() {
if (!torrentManager.isValid()) {
return false;
}
File torrent = torrentFile.get();
File torrentParent = torrent.getParentFile();
File torrentDownloadFolder = torrentManager.getTorrentManagerSettings()
.getTorrentDownloadFolder();
File torrentUploadFolder = torrentManager.getTorrentManagerSettings()
.getTorrentUploadsFolder();
if (!torrentParent.equals(torrentDownloadFolder)
&& !torrentParent.equals(torrentUploadFolder)) {
// if the torrent file is not located in the incomplete or upload
// directories it should be copied to the directory the torrent is
// being downloaded to. This is to prevent the user from deleting
// the torrent which we need to initiate a download properly.
File newTorrentFile = new File(torrentDownloadFolder, getName() + ".torrent");
FileUtils.copy(torrentFile.get(), newTorrentFile);
torrentFile.set(newTorrentFile);
}
torrentManager.registerTorrent(this);
return true;
}
@Override
public int getNumConnections() {
TorrentStatus status = getStatus();
if (status != null) {
return status.getNumConnections();
}
return 0;
}
@Override
public boolean isPrivate() {
return isPrivate;
}
@Override
public List<TorrentFileEntry> getTorrentFileEntries() {
if (cancelled.get()) {
TorrentInfo torrentInfo = this.torrentInfo.get();
if (torrentInfo == null) {
return Collections.emptyList();
}
return torrentInfo.getTorrentFileEntries();
}
return torrentManager.getTorrentFileEntries(this);
}
@Override
public List<TorrentPeer> getTorrentPeers() {
return torrentManager.getTorrentPeers(this);
}
@Override
public boolean isAutoManaged() {
TorrentStatus status = this.status.get();
return status == null ? false : status.isAutoManaged();
}
@Override
public void setAutoManaged(boolean autoManaged) {
torrentManager.setAutoManaged(this, autoManaged);
}
@Override
public void setTorrenFileEntryPriority(TorrentFileEntry torrentFileEntry, int priority) {
torrentManager.setTorrenFileEntryPriority(this, torrentFileEntry, priority);
}
@Override
public File getTorrentDataFile(TorrentFileEntry torrentFileEntry) {
return new File(getTorrentDataFile().getParent(), torrentFileEntry.getPath());
}
@Override
public void setTorrentInfo(TorrentInfo torrentInfo) {
this.torrentInfo.set(torrentInfo);
listeners.broadcast(TorrentEvent.META_DATA_UPDATED);
}
@Override
public boolean hasMetaData() {
return torrentInfo.get() != null;
}
@Override
public void initFiles() {
for (TorrentFileEntry fileEntry : getTorrentFileEntries()) {
File file = getTorrentDataFile(fileEntry);
if (!file.exists()) {
file.getParentFile().mkdirs();
try {
file.createNewFile();
} catch (IOException e) {
// should be able to continue on most platforms, libtorrent
// will create as needed.
LOG.warnf("Error creating file: {0}", file);
}
}
}
}
}