package com.limegroup.bittorrent;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.limewire.bittorrent.Torrent;
import org.limewire.bittorrent.TorrentEvent;
import org.limewire.bittorrent.TorrentFileEntry;
import org.limewire.bittorrent.TorrentInfo;
import org.limewire.bittorrent.TorrentParams;
import org.limewire.bittorrent.TorrentPeer;
import org.limewire.bittorrent.TorrentState;
import org.limewire.bittorrent.TorrentStatus;
import org.limewire.bittorrent.util.TorrentUtil;
import org.limewire.core.api.download.SaveLocationManager;
import org.limewire.core.settings.SharingSettings;
import org.limewire.i18n.I18nMarker;
import org.limewire.inspection.DataCategory;
import org.limewire.inspection.InspectablePrimitive;
import org.limewire.io.Address;
import org.limewire.io.ConnectableImpl;
import org.limewire.io.GUID;
import org.limewire.io.InvalidDataException;
import org.limewire.io.IpPortImpl;
import org.limewire.listener.AsynchronousMulticasterImpl;
import org.limewire.listener.EventListener;
import org.limewire.listener.EventMulticaster;
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.Provider;
import com.google.inject.name.Named;
import com.limegroup.gnutella.DownloadCallback;
import com.limegroup.gnutella.DownloadManager;
import com.limegroup.gnutella.InsufficientDataException;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.downloader.AbstractCoreDownloader;
import com.limegroup.gnutella.downloader.DownloadStateEvent;
import com.limegroup.gnutella.downloader.DownloaderType;
import com.limegroup.gnutella.downloader.IncompleteFileManager;
import com.limegroup.gnutella.downloader.serial.BTDownloadMemento;
import com.limegroup.gnutella.downloader.serial.BTMetaInfoMemento;
import com.limegroup.gnutella.downloader.serial.DownloadMemento;
import com.limegroup.gnutella.downloader.serial.LibTorrentBTDownloadMemento;
import com.limegroup.gnutella.downloader.serial.LibTorrentBTDownloadMementoImpl;
import com.limegroup.gnutella.library.FileCollection;
import com.limegroup.gnutella.library.GnutellaFiles;
import com.limegroup.gnutella.library.Library;
import com.limegroup.gnutella.malware.DangerousFileChecker;
/**
* Wraps the Torrent class in the Downloader interface to enable the gui to
* treat the torrent downloader as a normal downloader.
*/
public class BTDownloaderImpl extends AbstractCoreDownloader implements BTDownloader,
EventListener<TorrentEvent> {
private static final Log LOG = LogFactory.getLog(BTDownloaderImpl.class);
private static final String DANGEROUS_TORRENT_WARNING = I18nMarker.marktr(
"This torrent may have been designed to damage your computer.\n" +
"LimeWire has cancelled the download for your protection.");
@InspectablePrimitive(value = "number of torrents started", category = DataCategory.USAGE)
private static final AtomicInteger torrentsStarted = new AtomicInteger();
@InspectablePrimitive(value = "number of torrents finished", category = DataCategory.USAGE)
private static final AtomicInteger torrentsFinished = new AtomicInteger();
private final DownloadManager downloadManager;
private final Torrent torrent;
private final BTUploaderFactory btUploaderFactory;
private final AtomicBoolean finishing = new AtomicBoolean(false);
private final AtomicBoolean complete = new AtomicBoolean(false);
private final Library library;
private final EventMulticaster<DownloadStateEvent> listeners;
private final AtomicReference<DownloadState> lastState = new AtomicReference<DownloadState>(DownloadState.QUEUED);
private final FileCollection gnutellaFileCollection;
private final Provider<TorrentUploadManager> torrentUploadManager;
private final Provider<DangerousFileChecker> dangerousFileChecker;
private final Provider<DownloadCallback> downloadCallback;
/**
* Torrent info hash based URN used as a cache for getSha1Urn().
*/
private volatile URN urn = null;
@Inject
BTDownloaderImpl(SaveLocationManager saveLocationManager,
DownloadManager downloadManager,
BTUploaderFactory btUploaderFactory,
Provider<Torrent> torrentProvider,
Library library,
@Named("fastExecutor") ScheduledExecutorService fastExecutor,
@GnutellaFiles FileCollection gnutellaFileCollection,
Provider<TorrentUploadManager> torrentUploadManager,
Provider<DangerousFileChecker> dangerousFileChecker,
Provider<DownloadCallback> downloadCallback) {
super(saveLocationManager);
this.downloadManager = downloadManager;
this.btUploaderFactory = btUploaderFactory;
this.torrent = torrentProvider.get();
this.library = library;
this.gnutellaFileCollection = gnutellaFileCollection;
this.listeners = new AsynchronousMulticasterImpl<DownloadStateEvent>(fastExecutor);
this.torrentUploadManager = torrentUploadManager;
this.dangerousFileChecker = dangerousFileChecker;
this.downloadCallback = downloadCallback;
}
/**
* Registers the a listener on the torrent to update internal state of the
* downloader, based on updates to the torrent.
*/
@Inject
public void registerTorrentListener() {
torrent.addListener(this);
}
@Override
public void handleEvent(TorrentEvent event) {
if (TorrentEvent.COMPLETED == event && !complete.get()) {
finishing.set(true);
torrentsFinished.incrementAndGet();
if (checkForDangerousFiles()) {
return;
}
FileUtils.forceDeleteRecursive(getSaveFile());
File completeDir = getSaveFile().getParentFile();
torrent.moveTorrent(completeDir);
createUploadMemento();
cleanupPriorityZeroFiles();
File completeFile = getSaveFile();
addFileToCollections(completeFile);
complete.set(true);
deleteIncompleteFiles();
lastState.set(DownloadState.COMPLETE);
listeners.broadcast(new DownloadStateEvent(this, DownloadState.COMPLETE));
BTDownloaderImpl.this.downloadManager.remove(BTDownloaderImpl.this, true);
} else if (TorrentEvent.STOPPED == event) {
torrent.removeListener(this);
lastState.set(DownloadState.ABORTED);
listeners.broadcast(new DownloadStateEvent(this, DownloadState.ABORTED));
BTDownloaderImpl.this.downloadManager.remove(BTDownloaderImpl.this, true);
} else if (TorrentEvent.FAST_RESUME_FILE_SAVED == event) {
// nothing to do now.
} else if(TorrentEvent.STARTED == event) {
torrentsStarted.incrementAndGet();
} else if (TorrentEvent.META_DATA_UPDATED == event) {
if(!finishing.get() && !complete.get()) {
//TODO should be able to remove once the libtorrent alert bug is fixed.
torrent.initFiles();
}
} else {
DownloadState currentState = getState();
if (lastState.getAndSet(currentState) != currentState) {
listeners.broadcast(new DownloadStateEvent(this, currentState));
}
}
}
private void createUploadMemento() {
try {
torrentUploadManager.get().writeMemento(torrent);
torrent.setAutoManaged(true);
} catch (IOException e) {
LOG.error("Error saving torrent upload menento for torrent: " + torrent.getName(), e);
// non-fatal, upload will just not be loaded on application
// restart
}
}
/**
* Returns true if there are any Dangerous Files in this torrent after
* warning the user about them.
*/
private boolean checkForDangerousFiles() {
// If the torrent contains any dangerous files, delete everything
// and inform the user that the download has been cancelled.
for(File f : getIncompleteFiles()) {
if(dangerousFileChecker.get().isDangerous(f)) {
torrent.stop();
listeners.broadcast(new DownloadStateEvent(this, DownloadState.DANGEROUS));
downloadCallback.get().warnUser(getSaveFile().getName(),
DANGEROUS_TORRENT_WARNING);
return true;
}
}
return false;
}
/**
* Checks to see if this torrent has any priority zero files and removes
* them.
*/
private void cleanupPriorityZeroFiles() {
boolean hasAnyPriorityZero = false;
List<TorrentFileEntry> fileEntries = torrent.getTorrentFileEntries();
for (TorrentFileEntry fileEntry : fileEntries) {
if (fileEntry.getPriority() == 0) {
hasAnyPriorityZero = true;
break;
}
}
if (hasAnyPriorityZero) {
torrent.stop();// TODO for now not seeding paritally downloaded
// torrents, we can make the last pieces of the files
// priority zero potentially so it will not
// download/seed those pieces, need to look into
// this.
// TODO should maybe not use the same TorrentFileEntry for items in
// the TorrentInfo object, right now making a copy of the last known
// contents, to display the correct value in the ui, in general
// these fields are not kept up to date however.
TorrentInfo torrentInfo = new TorrentInfo(fileEntries);
torrent.setTorrentInfo(torrentInfo);
for (TorrentFileEntry fileEntry : fileEntries) {
if (fileEntry.getPriority() == 0) {
File torrentDataFile = torrent.getTorrentDataFile(fileEntry);
FileUtils.forceDelete(torrentDataFile);
}
}
}
}
/**
* Adds the torrents files to the gnutella share list if the torrent is not
* private and sharing is enabled, otehrwise the files are added to the
* library.
*/
private void addFileToCollections(File completeFile) {
if (completeFile.isDirectory()) {
FileFilter torrentFileFilter = new FileFilter() {
@Override
public boolean accept(File file) {
// library addFile method will filter out any truly
// unaddable files.
return true;
}
};
library.addFolder(completeFile, torrentFileFilter);
if (!torrent.isPrivate()
&& SharingSettings.SHARE_DOWNLOADED_FILES_IN_NON_SHARED_DIRECTORIES.getValue()) {
gnutellaFileCollection.addFolder(completeFile, torrentFileFilter);
}
} else {
library.add(completeFile);
if (!torrent.isPrivate()
&& SharingSettings.SHARE_DOWNLOADED_FILES_IN_NON_SHARED_DIRECTORIES.getValue()) {
gnutellaFileCollection.add(completeFile);
}
}
};
/**
* initializes this downloader from the given torrent file.
*/
@Override
public void init(File torrentFile, File saveDirectory) throws IOException {
torrent.init(new TorrentParams(torrentFile));
setDefaultFileName(torrent.getName());
}
@Override
public boolean registerTorrentWithTorrentManager() {
return torrent.registerWithTorrentManager();
}
/**
* Stops a torrent download. If the torrent is in seeding state, it does
* nothing. (To stop a seeding torrent it must be stopped from the uploads
* pane)
*/
@Override
public void stop() {
if (!torrent.isFinished()) {
torrent.stop();
downloadManager.remove(this, true);
} else {
downloadManager.remove(this, true);
}
}
@Override
public void pause() {
torrent.pause();
}
@Override
public boolean isPaused() {
return torrent.isPaused();
}
@Override
public boolean isPausable() {
return !isPaused();
}
@Override
public boolean isInactive() {
return isResumable() || getState() == DownloadState.QUEUED;
}
@Override
public boolean isLaunchable() {
return torrent.isSingleFileTorrent();
// TODO old logic would check last verified offest, but logic seems
// wrong, wince the pieces download randomly, there is no guarentee that
// the begginning of the file is ok to preview.
}
@Override
public boolean isResumable() {
return isPaused();
}
@Override
public boolean resume() {
torrent.resume();
return true;
}
@Override
public File getFile() {
if (torrent.isFinished()) {
return getSaveFile();
} else {
return getIncompleteFile();
}
}
@Override
public File getIncompleteFile() {
return new File(SharingSettings.INCOMPLETE_DIRECTORY.get(), torrent.getName());
}
@Override
public File getDownloadFragment() {
if (isCompleted()) {
return getSaveFile();
}
if (!isLaunchable()) {
return null;
}
if (torrent.isMultiFileTorrent()) {
return null;
}
File file = new File(getIncompleteFile().getParent(), IncompleteFileManager.PREVIEW_PREFIX
+ getIncompleteFile().getName());
// TODO come up with correct size for preview, look at old code checking
// last verified offsets etc. old code looks wrong though, since the
// file downloads randomly, the last verified offset does not tell us
// much.
long size = Math.min(getIncompleteFile().length(), 2 * 1024 * 1024);
if (FileUtils.copy(getIncompleteFile(), size, file) <= 0) {
return null;
}
return file;
}
@Override
public DownloadState getState() {
TorrentStatus status = torrent.getStatus();
if (!torrent.isStarted() || status == null) {
return DownloadState.QUEUED;
}
TorrentState state = status.getState();
// complete must be before aborted in order to not remove the download
// from the list prematurely when teh seed ratio is reached and the
// torrent is marked as cancelled.
if (torrent.isFinished()) {
return DownloadState.COMPLETE;
}
if (torrent.isCancelled()) {
return DownloadState.ABORTED;
}
if (status.isError()) {
// gave up maps to stalled in the core api, which is a recoverable
// error. All torrent downlaods are recoverable.
return DownloadState.GAVE_UP;
}
if (finishing.get()) {
return DownloadState.SAVING;
}
return convertState(state);
}
private DownloadState convertState(TorrentState state) {
switch (state) {
case DOWNLOADING:
if (isPaused()) {
return DownloadState.PAUSED;
} else {
return DownloadState.DOWNLOADING;
}
case QUEUED_FOR_CHECKING:
return DownloadState.RESUMING;
case CHECKING_FILES:
return DownloadState.RESUMING;
case SEEDING:
return DownloadState.COMPLETE;
case FINISHED:
return DownloadState.COMPLETE;
case ALLOCATING:
return DownloadState.CONNECTING;
case DOWNLOADING_METADATA:
return DownloadState.INITIALIZING;
default:
throw new IllegalStateException("Unknown libtorrent state: " + state);
}
}
@Override
public int getRemainingStateTime() {
// Unused
return 0;
}
@Override
public long getContentLength() {
TorrentStatus status = torrent.getStatus();
long contentLength = status != null ? status.getTotalWanted() : -1;
return contentLength;
}
@Override
public long getAmountRead() {
TorrentStatus status = torrent.getStatus();
if (status == null) {
return -1;
} else {
return status.getTotalWantedDone();
}
}
@Override
public String getVendor() {
return BITTORRENT_DOWNLOAD;
}
@Override
public void discardCorruptDownload(boolean delete) {
// we never give up because of corruption (because this can never be
// called)
}
@Override
public List<RemoteFileDesc> getRemoteFileDescs() {
return Collections.emptyList();
}
@Override
public int getQueuePosition() {
return 1;
}
@Override
public int getNumberOfAlternateLocations() {
return getPossibleHostCount();
}
@Override
public int getNumberOfInvalidAlternateLocations() {
return 0; // not applicable to torrents
}
@Override
public int getPossibleHostCount() {
return torrent.getNumPeers();
}
@Override
public int getBusyHostCount() {
return 0;
}
@Override
public int getQueuedHostCount() {
return 0;
}
@Override
public GUID getQueryGUID() {
// Unused for torrents
return null;
}
@Override
public boolean isCompleted() {
return complete.get();
}
@Override
public boolean shouldBeRemoved() {
switch (getState()) {
case ABORTED:
case COMPLETE:
return true;
}
return false;
}
@Override
public long getAmountVerified() {
TorrentStatus status = torrent.getStatus();
if (status == null) {
return -1;
} else {
return status.getTotalWantedDone();
}
}
@Override
public void measureBandwidth() {
// Unused, we are using the bandwidth reported by libtorrent
}
@Override
public float getMeasuredBandwidth() throws InsufficientDataException {
return (torrent.getDownloadRate() / 1024);
}
@Override
public float getAverageBandwidth() {
// Unused by anything
return (torrent.getDownloadRate() / 1024);
}
@Override
public boolean isRelocatable() {
return !isCompleted();
}
@Override
protected File getDefaultSaveFile() {
return new File(SharingSettings.getSaveDirectory(), torrent.getName());
}
@Override
public URN getSha1Urn() {
if (urn == null) {
synchronized (this) {
if (urn == null) {
try {
urn = URN.createSha1UrnFromHex(torrent.getSha1());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
return urn;
}
@Override
public int getNumHosts() {
return torrent.getNumPeers();
}
@Override
public List<Address> getSourcesAsAddresses() {
List<Address> list = new LinkedList<Address>();
List<TorrentPeer> peers = torrent.getTorrentPeers();
for (TorrentPeer peer : peers) {
String ip = peer.getIPAddress();
try {
list.add(new ConnectableImpl(new IpPortImpl(ip), false));
} catch (UnknownHostException e) {
// Discard invalid host
}
}
return list;
}
@Override
public void initialize() {
}
@Override
public void startDownload() {
btUploaderFactory.createBTUploader(torrent);
torrent.start();
}
@Override
public void handleInactivity() {
// nothing happens when we're inactive
}
@Override
public boolean shouldBeRestarted() {
return true;
}
@Override
public boolean isAlive() {
return false; // doesn't apply to torrents
}
@Override
public boolean isQueuable() {
return !isPaused();
}
@Override
public synchronized void finish() {
deleteIncompleteFiles();
}
@Override
public String getCustomIconDescriptor() {
return BITTORRENT_DOWNLOAD;
}
@Override
public DownloaderType getDownloadType() {
return DownloaderType.BTDOWNLOADER;
}
@Override
protected DownloadMemento createMemento() {
return new LibTorrentBTDownloadMementoImpl();
}
@Override
protected void fillInMemento(DownloadMemento memento) {
super.fillInMemento(memento);
LibTorrentBTDownloadMemento btMemento = (LibTorrentBTDownloadMemento) memento;
btMemento.setName(torrent.getName());
btMemento.setSha1Urn(getSha1Urn());
btMemento.setIncompleteFile(getIncompleteFile());
btMemento.setTrackerURL(torrent.getTrackerURL());
File fastResumeFile = torrent.getFastResumeFile();
String fastResumePath = fastResumeFile != null ? fastResumeFile.getAbsolutePath() : null;
btMemento.setFastResumePath(fastResumePath);
File torrentFile = torrent.getTorrentFile();
String torrentPath = torrentFile != null ? torrentFile.getAbsolutePath() : null;
btMemento.setTorrentPath(torrentPath);
btMemento.setPrivate(torrent.isPrivate());
}
public void initFromCurrentMemento(LibTorrentBTDownloadMemento memento)
throws InvalidDataException {
urn = memento.getSha1Urn();
if (urn == null) {
throw new InvalidDataException(
"Null SHA1 URN retrieved from LibTorrent torrent momento.");
}
if (!urn.isSHA1()) {
throw new InvalidDataException(
"Non SHA1 URN retrieved from LibTorrent torrent momento.");
}
String fastResumePath = memento.getFastResumePath();
File fastResumeFile = fastResumePath != null ? new File(fastResumePath) : null;
String torrentPath = memento.getTorrentPath();
File torrentFile = torrentPath != null ? new File(torrentPath) : null;
try {
TorrentParams params = new TorrentParams(memento.getName(), StringUtils.toHexString(urn
.getBytes()));
params.trackerURL(memento.getTrackerURL()).fastResumeFile(fastResumeFile).torrentFile(
torrentFile).torrentDataFile(memento.getIncompleteFile()).isPrivate(
memento.isPrivate());
torrent.init(params);
} catch (IOException e) {
// the .torrent file could be invalid, try to initialize just with
// the memento contents.
try {
TorrentParams params = new TorrentParams(memento.getName(), StringUtils
.toHexString(urn.getBytes()));
params.trackerURL(memento.getTrackerURL()).fastResumeFile(fastResumeFile)
.torrentDataFile(memento.getIncompleteFile())
.isPrivate(memento.isPrivate());
torrent.init(params);
} catch (IOException e1) {
throw new InvalidDataException("Could not initialize the BTDownloader", e1);
}
}
}
public void initFromOldMemento(BTDownloadMemento memento) throws InvalidDataException {
BTMetaInfoMemento btmetainfo = memento.getBtMetaInfoMemento();
URI[] trackers = btmetainfo.getTrackers();
URI tracker1 = trackers[0];
String name = btmetainfo.getFileSystem().getName();
byte[] infoHash = btmetainfo.getInfoHash();
String sha1 = StringUtils.toHexString(infoHash);
boolean isPrivate = btmetainfo.isPrivate();
File saveFile = memento.getSaveFile();
File saveDir = saveFile == null ? SharingSettings.getSaveDirectory() : saveFile
.getParentFile();
saveDir = saveDir == null ? SharingSettings.getSaveDirectory() : saveDir;
File oldIncompleteFile = btmetainfo.getFileSystem().getIncompleteFile();
File newIncompleteFile = new File(SharingSettings.INCOMPLETE_DIRECTORY.get(), name);
if (newIncompleteFile.exists()) {
throw new InvalidDataException(
"Cannot init memento for BTDownloader, incomplete file already exists: "
+ newIncompleteFile);
}
FileUtils.forceRename(oldIncompleteFile, newIncompleteFile);
File torrentDir = oldIncompleteFile.getParentFile();
if (torrentDir.getName().length() == 32) {
// looks like the old torrent dir
FileUtils.forceDeleteRecursive(torrentDir);
}
try {
TorrentParams params = new TorrentParams(name, sha1);
params.trackerURL(tracker1.toString()).torrentDataFile(newIncompleteFile).isPrivate(
isPrivate);
torrent.init(params);
} catch (IOException e) {
throw new InvalidDataException("Could not initialize the BTDownloader", e);
}
}
@Override
public synchronized void initFromMemento(DownloadMemento memento) throws InvalidDataException {
super.initFromMemento(memento);
if (BTDownloadMemento.class.isInstance(memento)) {
initFromOldMemento((BTDownloadMemento) memento);
} else if (LibTorrentBTDownloadMemento.class.isInstance(memento)) {
initFromCurrentMemento((LibTorrentBTDownloadMemento) memento);
}
if (!registerTorrentWithTorrentManager())
throw new InvalidDataException("Error registering torrent");
}
/**
* Adds basic DownloadStateEvent listener support. Currently only
* broadcasts, COMPLETED and ABORTED states.
*/
@Override
public void addListener(EventListener<DownloadStateEvent> listener) {
listeners.addListener(listener);
}
@Override
public boolean removeListener(EventListener<DownloadStateEvent> listener) {
return listeners.removeListener(listener);
}
@Override
public void deleteIncompleteFiles() {
if (!complete.get()) {
File incompleteFile = getIncompleteFile();
if (incompleteFile != null) {
FileUtils.forceDeleteRecursive(incompleteFile);
}
}
}
@Override
public List<File> getCompleteFiles() {
return TorrentUtil.buildTorrentFiles(torrent, getSaveFile().getParentFile());
}
public List<File> getIncompleteFiles() {
return TorrentUtil.buildTorrentFiles(torrent, getIncompleteFile().getParentFile());
}
@Override
public boolean conflicts(URN urn, long fileSize, File... file) {
if (getSha1Urn().equals(urn)) {
return true;
}
for (File f : file) {
if (conflictsSaveFile(f)) {
return true;
}
}
return false;
}
@Override
public boolean conflictsSaveFile(File complete) {
return complete.equals(getSaveFile());
}
@Override
public boolean conflictsWithIncompleteFile(File incomplete) {
return incomplete.equals(getIncompleteFile());
}
/**
* No longer relevant in any Downloader.
*/
@Override
public int getChunkSize() {
throw new UnsupportedOperationException("BTDownloaderImpl.getChunkSize() not implemented");
}
/**
* No longer relevant in any Downloader.
*/
@Override
public long getAmountLost() {
throw new UnsupportedOperationException("BTDownloaderImpl.getAmountLost() not implemented");
}
/**
* No longer relevant in any Downloader.
*/
@Override
public int getAmountPending() {
throw new UnsupportedOperationException(
"BTDownloaderImpl.getAmountPending() not implemented");
}
/**
* No longer relevant in any Downloader.
*/
@Override
public int getTriedHostCount() {
throw new UnsupportedOperationException(
"BTDownloaderImpl.getTriedHostCount() not implemented");
}
@Override
public File getTorrentFile() {
return torrent.getTorrentFile();
}
public Torrent getTorrent() {
return torrent;
}
}