package com.limegroup.bittorrent;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import org.limewire.bittorrent.BTData;
import org.limewire.bittorrent.BTDataImpl;
import org.limewire.bittorrent.ProxySetting;
import org.limewire.bittorrent.ProxySettingType;
import org.limewire.bittorrent.Torrent;
import org.limewire.bittorrent.TorrentEvent;
import org.limewire.bittorrent.TorrentEventType;
import org.limewire.bittorrent.TorrentIpFilter;
import org.limewire.bittorrent.TorrentManager;
import org.limewire.bittorrent.TorrentManagerSettings;
import org.limewire.bittorrent.TorrentParams;
import org.limewire.bittorrent.TorrentStatus;
import org.limewire.bittorrent.TorrentTrackerScraper.ScrapeCallback;
import org.limewire.bittorrent.bencoding.Token;
import org.limewire.core.settings.BittorrentSettings;
import org.limewire.core.settings.ConnectionSettings;
import org.limewire.core.settings.SharingSettings;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.IP;
import org.limewire.libtorrent.LibTorrentSession;
import org.limewire.lifecycle.Service;
import org.limewire.lifecycle.ServiceRegistry;
import org.limewire.listener.EventListener;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.util.FileUtils;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.limegroup.gnutella.filters.IPFilter;
import com.limegroup.gnutella.library.FileCollection;
import com.limegroup.gnutella.library.GnutellaFiles;
/**
* Lazy TorrentManager wraps the TorrentManagerImpl and allows holding off
* initializing against the native libraries until the first time and methods
* are called on the torrent manager.
*
* It registers itself as a service to still enabled proper shutdown of the
* Torrent code, but cleanup only needs to be done is the underlying
* implementation was initialized.
*
*/
@EagerSingleton
public class LimeWireTorrentManager implements TorrentManager, Service {
private static final Log LOG = LogFactory.getLog(LimeWireTorrentManager.class);
private final FileCollection gnutellaFileList;
private final Provider<LibTorrentSession> torrentManager;
private final EventListener<TorrentEvent> torrentListener = new EventListener<TorrentEvent>() {
@Override
public void handleEvent(TorrentEvent event) {
handleTorrentEvent(event);
}
};
private volatile boolean initialized = false;
private final IpFilterPredicate ipFilterPredicate;
@Inject
public LimeWireTorrentManager(Provider<LibTorrentSession> torrentManager,
@GnutellaFiles FileCollection gnutellaFileList, IPFilter ipFilter) {
this.torrentManager = torrentManager;
this.ipFilterPredicate = new IpFilterPredicate(ipFilter);
this.gnutellaFileList = gnutellaFileList;
}
private void handleTorrentEvent(TorrentEvent event) {
if (event.getType() == TorrentEventType.COMPLETED) {
limitSeedingTorrents();
} else if (event.getType() == TorrentEventType.STOPPED) {
event.getTorrent().removeListener(torrentListener);
}
}
@Inject
public void register(ServiceRegistry serviceRegistry) {
serviceRegistry.register(this);
}
private void setupTorrentManager() {
if (!initialized) {
synchronized (this) {
if (!initialized) {
try {
this.torrentManager.get().initialize();
if (torrentManager.get().isValid()) {
updateProxies();
torrentManager.get().setIpFilter(ipFilterPredicate);
if (BittorrentSettings.TORRENT_USE_UPNP.getValue()) {
torrentManager.get().startUPnP();
} else {
torrentManager.get().stopUPnP();
}
this.torrentManager.get().start();
this.torrentManager.get().scheduleWithFixedDelay(
new TorrentDHTScheduler(this), 1000, 60 * 1000,
TimeUnit.MILLISECONDS);
this.torrentManager.get().scheduleWithFixedDelay(
new TorrentResumeDataScheduler(this), 10000, 10000,
TimeUnit.MILLISECONDS);
}
} finally {
initialized = true;
}
}
}
}
}
@Override
public String getServiceName() {
return "TorrentManager";
}
@Override
public TorrentManagerSettings getTorrentManagerSettings() {
// not calling setup because we don't want to initialize the library
// here.
// settings can be gotten without initialization.
return torrentManager.get().getTorrentManagerSettings();
}
@Override
public void initialize() {
// handled in setup method.
}
@Override
public void setIpFilter(TorrentIpFilter ipFilterPredicate) {
setupTorrentManager();
torrentManager.get().setIpFilter(ipFilterPredicate);
}
@Override
public boolean isDownloadingTorrent(File torrentFile) {
if (!initialized) {
return false;
}
setupTorrentManager();
return torrentManager.get().isDownloadingTorrent(torrentFile);
}
@Override
public Torrent getTorrent(File torrentFile) {
if (!initialized) {
return null;
}
setupTorrentManager();
return torrentManager.get().getTorrent(torrentFile);
}
@Override
public Torrent getTorrent(String sha1) {
if (!initialized) {
return null;
}
setupTorrentManager();
return torrentManager.get().getTorrent(sha1);
}
@Override
public boolean isValid() {
setupTorrentManager();
return torrentManager.get().isValid();
}
/**
* Shares the torrent with gnutella, then registers the specified torrent
* with the TorrentManager. Delegates an add torrent call to the underlying
* torrentManager implementation.
*
* @return the torrent if it was successfully added, null otherwise.
*/
@Override
public Torrent addTorrent(TorrentParams params) throws IOException {
if (!isValid()) {
return null;
}
params.fill();
shareTorrent(params.getTorrentFile());
return addTorrentInternal(params);
}
private Torrent addTorrentInternal(TorrentParams params) throws IOException {
File torrentFile = params.getTorrentFile();
if (torrentFile != null) {
File torrentParent = torrentFile.getParentFile();
File torrentDownloadFolder = SharingSettings.INCOMPLETE_DIRECTORY.get();
File torrentUploadFolder = BittorrentSettings.TORRENT_UPLOADS_FOLDER.get();
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.
torrentDownloadFolder.mkdirs();
File newTorrentFile = new File(torrentDownloadFolder, params.getName() + ".torrent");
FileUtils.copy(torrentFile, newTorrentFile);
params.setTorrentFile(newTorrentFile);
}
}
return torrentManager.get().addTorrent(params);
}
/**
* Same as addTorrent but the torrent file will not be shared with gnutella.
* Additionally in the future we will probably change some settings for the
* individual torrent, like max uploads/download speeds.
*
* @return the torrent if it was successfully added, null otherwise.
*/
public Torrent seedTorrent(TorrentParams params) throws IOException {
if (!isValid()) {
return null;
}
params.fill();
return addTorrentInternal(params);
}
@Override
public void removeTorrent(Torrent torrent) {
setupTorrentManager();
torrentManager.get().removeTorrent(torrent);
torrent.removeListener(torrentListener);
}
@Override
public void start() {
// handled in setup method.
}
@Override
public void stop() {
synchronized (this) {
try {
if (initialized && torrentManager.get().isValid()) {
torrentManager.get().stop();
}
} finally {
initialized = true;
}
}
}
@Override
public void setTorrentManagerSettings(TorrentManagerSettings settings) {
setupTorrentManager();
torrentManager.get().setTorrentManagerSettings(settings);
updateProxies();
limitSeedingTorrents();
}
private void updateProxies() {
ProxySetting proxy = buildProxySetting();
torrentManager.get().setPeerProxy(proxy);
torrentManager.get().setTrackerProxy(proxy);
torrentManager.get().setWebSeedProxy(proxy);
torrentManager.get().setDHTProxy(proxy);
}
private ProxySetting buildProxySetting() {
return new ProxySetting() {
@Override
public String getHostname() {
return ConnectionSettings.PROXY_HOST.get();
}
@Override
public int getPort() {
return ConnectionSettings.PROXY_PORT.getValue();
}
@Override
public String getUsername() {
return ConnectionSettings.PROXY_USERNAME.get();
}
@Override
public String getPassword() {
switch (ConnectionSettings.CONNECTION_METHOD.getValue()) {
case ConnectionSettings.C_SOCKS4_PROXY:
return "";
default:
return ConnectionSettings.PROXY_PASS.get();
}
}
@Override
public ProxySettingType getType() {
boolean authenticate = ConnectionSettings.PROXY_AUTHENTICATE.getValue();
switch (ConnectionSettings.CONNECTION_METHOD.getValue()) {
case ConnectionSettings.C_SOCKS4_PROXY:
return ProxySettingType.SOCKS4;
case ConnectionSettings.C_SOCKS5_PROXY:
if (authenticate) {
return ProxySettingType.SOCKS5_PW;
} else {
return ProxySettingType.SOCKS5;
}
case ConnectionSettings.C_HTTP_PROXY:
if (authenticate) {
return ProxySettingType.HTTP_PW;
} else {
return ProxySettingType.HTTP;
}
case ConnectionSettings.C_NO_PROXY:
default:
return null;
}
}
};
}
private static class IpFilterPredicate implements TorrentIpFilter {
private final IPFilter ipFilter;
IpFilterPredicate(IPFilter ipFilter) {
this.ipFilter = ipFilter;
}
@Override
public boolean allow(int ipAddress) {
return ipFilter.allow(new IP(ipAddress, -1));
}
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public List<Torrent> getTorrents() {
if (!initialized) {
return Collections.emptyList();
}
setupTorrentManager();
return torrentManager.get().getTorrents();
}
@Override
public Lock getLock() {
setupTorrentManager();
return torrentManager.get().getLock();
}
@Override
public boolean isDHTStarted() {
if (!initialized) {
return false;
}
setupTorrentManager();
return torrentManager.get().isDHTStarted();
}
@Override
public void startDHT(File dhtStateFile) {
setupTorrentManager();
torrentManager.get().startDHT(dhtStateFile);
}
@Override
public void stopDHT() {
setupTorrentManager();
torrentManager.get().stopDHT();
}
@Override
public void saveDHTState(File dhtStateFile) {
if (!initialized) {
return;
}
setupTorrentManager();
torrentManager.get().saveDHTState(dhtStateFile);
}
@Override
public boolean isUPnPStarted() {
if (!initialized) {
return false;
}
setupTorrentManager();
return torrentManager.get().isUPnPStarted();
}
@Override
public void startUPnP() {
setupTorrentManager();
torrentManager.get().startUPnP();
}
@Override
public void stopUPnP() {
setupTorrentManager();
torrentManager.get().stopUPnP();
}
private void limitSeedingTorrents() {
// Check the number of seeding torrents and stop any long running
// torrents
// if there are more there are more than the limit
torrentManager.get().getLock().lock();
try {
int seedingTorrents = 0;
int maxSeedingTorrents = BittorrentSettings.TORRENT_SEEDING_LIMIT.getValue();
if (BittorrentSettings.UPLOAD_TORRENTS_FOREVER.getValue()) {
maxSeedingTorrents = Integer.MAX_VALUE;
}
// Cut out early if the limit is infinite
if (maxSeedingTorrents == Integer.MAX_VALUE) {
return;
}
for (Torrent torrent : torrentManager.get().getTorrents()) {
if (torrent.isFinished()) {
seedingTorrents++;
}
}
if (seedingTorrents <= maxSeedingTorrents) {
return;
}
List<Torrent> ratioSortedTorrents = new ArrayList<Torrent>(torrentManager.get()
.getTorrents());
Collections.sort(ratioSortedTorrents, new Comparator<Torrent>() {
@Override
public int compare(Torrent o1, Torrent o2) {
// Sort smallest first
int compare = Double.compare(o2.getSeedRatio(), o1.getSeedRatio());
// Compare by seeding time if seeding ratio is the same
// (generally at 0:0)
// -- Older values are discarded first. --
if (compare == 0) {
TorrentStatus status1 = o1.getStatus();
TorrentStatus status2 = o2.getStatus();
if (status1 != null && status2 != null) {
int time1 = status1.getSeedingTime();
int time2 = status2.getSeedingTime();
if (time1 > time2) {
return -1;
} else if (time2 > time1) {
return 1;
} else {
return 0;
}
}
}
return compare;
}
});
for (int i = 0; i < seedingTorrents - maxSeedingTorrents
&& ratioSortedTorrents.size() > 0;) {
Torrent torrent = ratioSortedTorrents.remove(0);
if (torrent.isFinished()) {
torrent.stop();
i++;
}
}
} finally {
torrentManager.get().getLock().unlock();
}
}
@Override
public void setPeerProxy(ProxySetting proxy) {
setupTorrentManager();
torrentManager.get().setPeerProxy(proxy);
}
@Override
public void setDHTProxy(ProxySetting proxy) {
setupTorrentManager();
torrentManager.get().setDHTProxy(proxy);
}
@Override
public void setTrackerProxy(ProxySetting proxy) {
setupTorrentManager();
torrentManager.get().setTrackerProxy(proxy);
}
@Override
public void setWebSeedProxy(ProxySetting proxy) {
setupTorrentManager();
torrentManager.get().setWebSeedProxy(proxy);
}
private boolean shareTorrent(File torrentFile) {
if (torrentFile == null || !torrentFile.exists() || isDownloadingTorrent(torrentFile)) {
return true;
}
if (!SharingSettings.SHARE_DOWNLOADED_FILES_IN_NON_SHARED_DIRECTORIES.getValue()) {
return true;
}
BTData btData = null;
FileInputStream torrentInputStream = null;
try {
torrentInputStream = new FileInputStream(torrentFile);
Map<?, ?> torrentFileMap = (Map<?, ?>) Token.parse(torrentInputStream.getChannel());
btData = new BTDataImpl(torrentFileMap);
} catch (IOException e) {
LOG.error("Error reading torrent file: " + torrentFile, e);
return false;
} finally {
FileUtils.close(torrentInputStream);
}
if (btData.isPrivate()) {
gnutellaFileList.remove(torrentFile);
return true;
}
File saveDir = SharingSettings.getSaveDirectory();
File torrentParent = torrentFile.getParentFile();
if (torrentParent.equals(saveDir)) {
// already in saveDir
gnutellaFileList.add(torrentFile);
return true;
}
final File tFile = getSharedTorrentMetaDataFile(btData);
if (tFile.equals(torrentFile)) {
gnutellaFileList.add(tFile);
return true;
}
if (tFile.exists()) {
gnutellaFileList.add(tFile);
return true;
}
if (FileUtils.copy(torrentFile, tFile)) {
gnutellaFileList.add(tFile);
}
return true;
}
private File getSharedTorrentMetaDataFile(BTData btData) {
String fileName = btData.getName().concat(".torrent");
File f = new File(SharingSettings.getSaveDirectory(), fileName);
return f;
}
@Override
public void queueTrackerScrapeRequest(String hexSha1Urn, URI trackerUri, ScrapeCallback callback) {
setupTorrentManager();
torrentManager.get().queueTrackerScrapeRequest(hexSha1Urn, trackerUri, callback);
}
}