package com.limegroup.gnutella; import java.io.File; import java.io.IOException; import java.net.Socket; import java.net.URI; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.limewire.bittorrent.Torrent; import org.limewire.bittorrent.TorrentFileEntry; import org.limewire.bittorrent.TorrentInfo; import org.limewire.bittorrent.TorrentManager; import org.limewire.bittorrent.TorrentParams; import org.limewire.collection.MultiIterable; import org.limewire.core.api.Category; import org.limewire.core.api.download.DownloadException; import org.limewire.core.api.download.DownloadException.ErrorCode; import org.limewire.core.api.file.CategoryManager; import org.limewire.core.settings.BittorrentSettings; import org.limewire.core.settings.DownloadSettings; import org.limewire.core.settings.FilterSettings; import org.limewire.core.settings.LibrarySettings; import org.limewire.core.settings.SharingSettings; import org.limewire.i18n.I18nMarker; import org.limewire.inject.EagerSingleton; import org.limewire.io.Address; import org.limewire.io.GUID; import org.limewire.io.InvalidDataException; import org.limewire.io.IpPort; import org.limewire.libtorrent.LibTorrentParams; import org.limewire.lifecycle.Service; import org.limewire.lifecycle.ServiceStage; import org.limewire.listener.EventListener; import org.limewire.listener.EventListenerList; import org.limewire.listener.ListenerSupport; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.service.MessageService; import org.limewire.util.FileUtils; import org.limewire.util.StringUtils; import org.limewire.util.Visitor; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; import com.limegroup.bittorrent.BTDownloader; import com.limegroup.bittorrent.BTTorrentFileDownloader; import com.limegroup.bittorrent.TorrentUploadManager; import com.limegroup.gnutella.browser.MagnetOptions; import com.limegroup.gnutella.downloader.CantResumeException; import com.limegroup.gnutella.downloader.CoreDownloader; import com.limegroup.gnutella.downloader.CoreDownloaderFactory; import com.limegroup.gnutella.downloader.DownloaderType; import com.limegroup.gnutella.downloader.IncompleteFileManager; import com.limegroup.gnutella.downloader.MagnetDownloader; import com.limegroup.gnutella.downloader.ManagedDownloader; import com.limegroup.gnutella.downloader.PushDownloadManager; import com.limegroup.gnutella.downloader.PushedSocketHandlerRegistry; import com.limegroup.gnutella.downloader.RemoteFileDescFactory; import com.limegroup.gnutella.downloader.ResumeDownloader; import com.limegroup.gnutella.downloader.serial.DownloadMemento; import com.limegroup.gnutella.downloader.serial.DownloadSerializer; import com.limegroup.gnutella.library.LibraryStatusEvent; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.QueryReply; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.mozilla.MozillaDownload; import com.limegroup.mozilla.MozillaDownloaderImpl; @EagerSingleton public class DownloadManagerImpl implements DownloadManager, Service, EventListener<LibraryStatusEvent> { private static final Log LOG = LogFactory.getLog(DownloadManagerImpl.class); /** The time in milliseconds between checkpointing downloads.dat. The more * often this is written, the less the lost data during a crash, but the * greater the chance that downloads.dat itself is corrupt. */ private int SNAPSHOT_CHECKPOINT_TIME=30*1000; //30 seconds /** The list of all ManagedDownloader's attempting to download. * INVARIANT: active.size()<=slots() && active contains no duplicates * LOCKING: obtain this' monitor */ private final List <CoreDownloader> active=new LinkedList<CoreDownloader>(); /** The list of all queued ManagedDownloader. * INVARIANT: waiting contains no duplicates * LOCKING: obtain this' monitor */ private final List <CoreDownloader> waiting=new LinkedList<CoreDownloader>(); private final MultiIterable<CoreDownloader> activeAndWaiting = new MultiIterable<CoreDownloader>(active,waiting); /** * Whether or not the GUI has been init'd. */ private volatile boolean downloadsReadFromDisk = false; /** The number if IN-NETWORK active downloaders. We don't count these when * determining how many downloaders are active. */ private int innetworkCount = 0; /** * The number of active store downloads. These are counted when determining * how many downloaders are active */ private int storeDownloadCount = 0; private int mozillaDownloadCount = 0; /** * The number of times we've been bandwidth measures */ private int numMeasures = 0; /** * The average bandwidth over all downloads. * This is only counted while downloads are active. */ private float averageBandwidth = 0; /** The last measured bandwidth, as counted from measureBandwidth. */ private volatile float lastMeasuredBandwidth; /** * The runnable that pumps inactive downloads to the correct state. */ private Runnable _waitingPump; private final EventListenerList<DownloadManagerEvent> listeners = new EventListenerList<DownloadManagerEvent>(); private final Provider<DownloadCallback> downloadCallback; private final Provider<MessageRouter> messageRouter; private final ScheduledExecutorService backgroundExecutor; private final Provider<PushDownloadManager> pushDownloadManager; private final CoreDownloaderFactory coreDownloaderFactory; private final DownloadSerializer downloadSerializer; private final IncompleteFileManager incompleteFileManager; private final RemoteFileDescFactory remoteFileDescFactory; private final CategoryManager categoryManager; private final PushEndpointFactory pushEndpointFactory; private final Provider<TorrentManager> torrentManager; private final Provider<TorrentUploadManager> torrentUploadManager; @Inject public DownloadManagerImpl( Provider<DownloadCallback> downloadCallback, Provider<MessageRouter> messageRouter, @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor, Provider<PushDownloadManager> pushDownloadManager, CoreDownloaderFactory coreDownloaderFactory, DownloadSerializer downloaderSerializer, IncompleteFileManager incompleteFileManager, RemoteFileDescFactory remoteFileDescFactory, PushEndpointFactory pushEndpointFactory, Provider<TorrentManager> torrentManager, Provider<TorrentUploadManager> torrentUploadManager, CategoryManager categoryManager) { this.downloadCallback = downloadCallback; this.messageRouter = messageRouter; this.backgroundExecutor = backgroundExecutor; this.pushDownloadManager = pushDownloadManager; this.coreDownloaderFactory = coreDownloaderFactory; this.downloadSerializer = downloaderSerializer; this.incompleteFileManager = incompleteFileManager; this.remoteFileDescFactory = remoteFileDescFactory; this.pushEndpointFactory = pushEndpointFactory; this.torrentManager = torrentManager; this.torrentUploadManager = torrentUploadManager; this.categoryManager = categoryManager; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#register(com.limegroup.gnutella.downloader.PushedSocketHandlerRegistry) */ // DO NOT REMOVE! Guice calls this because of the @Inject @Inject public void register(PushedSocketHandlerRegistry registry) { registry.register(this); } @Inject void register(ListenerSupport<LibraryStatusEvent> listeners) { listeners.addListener(this); } //////////////////////// Creation and Saving ///////////////////////// @Inject void register(org.limewire.lifecycle.ServiceRegistry registry) { registry.register(this); registry.register(new Service() { public String getServiceName() { return org.limewire.i18n.I18nMarker.marktr("Old Downloads"); } public void initialize() { }; public void start() { loadSavedDownloadsAndScheduleWriting(); //TODO load uploads from somewhere else? torrentUploadManager.get().loadSavedUploads(); } public void stop() { }; }).in(ServiceStage.LATE); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#start() */ public void start() { scheduleWaitingPump(); } public String getServiceName() { return org.limewire.i18n.I18nMarker.marktr("Download Management"); } public void initialize() { } public void stop() { writeSnapshot(); } /** * Adds a new downloader to this manager. * @param downloader the core downloader */ public void addNewDownloader(CoreDownloader downloader) { initializeDownload(downloader, false); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#loadSavedDownloadsAndScheduleWriting() */ public void loadSavedDownloadsAndScheduleWriting() { loadSavedDownloads(); scheduleSnapshots(); } public void loadSavedDownloads() { boolean failedAll = true; boolean failedSome = false; List<DownloadMemento> mementos; try { mementos = downloadSerializer.readFromDisk(); if(mementos.isEmpty()) failedAll = false; } catch(IOException ioex) { mementos = Collections.emptyList(); } for(DownloadMemento memento : mementos) { CoreDownloader coreDownloader = prepareMemento(memento); if(coreDownloader != null) { failedAll = false; addNewDownloader(coreDownloader); } else { failedSome = true; } } loadResumeDownloaders(); downloadsReadFromDisk = true; if(failedAll) { MessageService.showError(I18nMarker.marktr("Sorry, LimeWire couldn't read your old downloads. You can restart them by clicking 'Try Again' on the downloads. When LimeWire finds a source for the file, the download will pick up where it left off.")); } else if(failedSome) { MessageService.showError(I18nMarker.marktr("Sorry, LimeWire couldn't read some of your old downloads. You can restart them by clicking 'Try Again' on the downloads. When LimeWire finds a source for the file, the download will pick up where it left off.")); } } private void loadResumeDownloaders() { Collection<File> incompleteFiles = incompleteFileManager.getUnregisteredIncompleteFilesInDirectory( SharingSettings.INCOMPLETE_DIRECTORY.get()); for(File file : incompleteFiles) { try { download(file); } catch (DownloadException e) { LOG.error("SLE loading incomplete file", e); } catch (CantResumeException e) { LOG.error("CRE loading incomplete file", e); } } } public CoreDownloader prepareMemento(DownloadMemento memento) { try { return coreDownloaderFactory.createFromMemento(memento); } catch(InvalidDataException ide) { LOG.warn("Unable to read download from memento: " + memento, ide); return null; } } public void scheduleSnapshots() { Runnable checkpointer=new Runnable() { public void run() { if (downloadsInProgress() > 0) { //optimization writeSnapshot(); } } }; backgroundExecutor.scheduleWithFixedDelay(checkpointer, SNAPSHOT_CHECKPOINT_TIME, SNAPSHOT_CHECKPOINT_TIME, TimeUnit.MILLISECONDS); } public void writeSnapshot() { if (!downloadsReadFromDisk) { LOG.debug("downloads not loaded yet, not writing snapshot"); return; } List<DownloadMemento> mementos; synchronized(this) { mementos = new ArrayList<DownloadMemento>(active.size() + waiting.size()); for(CoreDownloader downloader : activeAndWaiting) { if(downloader.isMementoSupported()) { mementos.add(downloader.toMemento()); } } } downloadSerializer.writeToDisk(mementos); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#isGUIInitd() */ public boolean isSavedDownloadsLoaded() { return downloadsReadFromDisk; } PushDownloadManager getPushManager() { return pushDownloadManager.get(); } /** * Delegates the incoming socket out to BrowseHostHandler & then attempts to assign it * to any ManagedDownloader. * * Closes the socket if neither BrowseHostHandler nor any ManagedDownloaders wanted it. * */ private synchronized boolean handleIncomingPush(String file, int index, byte [] clientGUID, Socket socket) { boolean handled = false; for (CoreDownloader md : activeAndWaiting) { if (! (md instanceof ManagedDownloader)) continue; // pushes apply to gnutella downloads only ManagedDownloader mmd = (ManagedDownloader)md; if (mmd.acceptDownload(file, socket, index, clientGUID)) { return true; } } return handled; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#acceptPushedSocket(java.lang.String, int, byte[], java.net.Socket) */ public boolean acceptPushedSocket(String file, int index, byte[] clientGUID, Socket socket) { return handleIncomingPush(file, index, clientGUID, socket); } public void scheduleWaitingPump() { if(_waitingPump != null) return; _waitingPump = new Runnable() { public void run() { pumpDownloads(); } }; backgroundExecutor.scheduleWithFixedDelay(_waitingPump, 1000, 1000, TimeUnit.MILLISECONDS); } /** * Pumps through each waiting download, either removing it because it was * stopped, or adding it because there's an active slot and it requires * attention. */ protected synchronized void pumpDownloads() { int index = 1; for(Iterator<CoreDownloader> i = waiting.iterator(); i.hasNext(); ) { CoreDownloader md = i.next(); if(md.isAlive()) { continue; } else if(md.shouldBeRemoved()) { i.remove(); cleanupCompletedDownload(md, false); } // handle downloads from LWS separately, only allow 3 at a time else if( storeDownloadCount < 3 && md.getDownloadType() == DownloaderType.STORE ) { i.remove(); storeDownloadCount++; active.add(md); md.startDownload(); } else if(hasFreeSlot() && (md.shouldBeRestarted()) && (md.getDownloadType() != DownloaderType.STORE)) { i.remove(); if(md.getDownloadType() == DownloaderType.INNETWORK) innetworkCount++; active.add(md); md.startDownload(); } else { if(md.isQueuable()) md.setInactivePriority(index++); md.handleInactivity(); } } } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#isIncomplete(com.limegroup.gnutella.URN) */ public boolean isIncomplete(URN urn) { return incompleteFileManager.getFileForUrn(urn) != null; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#isActivelyDownloading(com.limegroup.gnutella.URN) */ public boolean isActivelyDownloading(URN urn) { Downloader md = getDownloaderForURN(urn); if(md == null) return false; switch(md.getState()) { case QUEUED: case BUSY: case ABORTED: case GAVE_UP: case UNABLE_TO_CONNECT: case DISK_PROBLEM: case CORRUPT_FILE: case REMOTE_QUEUED: case WAITING_FOR_USER: case DANGEROUS: case THREAT_FOUND: case SCAN_FAILED: return false; default: return true; } } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getIncompleteFileManager() */ public IncompleteFileManager getIncompleteFileManager() { return incompleteFileManager; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#downloadsInProgress() */ public synchronized int downloadsInProgress() { return active.size() + waiting.size(); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getNumIndividualDownloaders() */ public synchronized int getNumIndividualDownloaders() { int ret = 0; for (Iterator<CoreDownloader> iter=active.iterator(); iter.hasNext(); ) { //active Object next = iter.next(); if (! (next instanceof ManagedDownloader)) continue; // TODO: count torrents separately ManagedDownloader md=(ManagedDownloader)next; ret += md.getNumDownloaders(); } return ret; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getNumActiveDownloads() */ public synchronized int getNumActiveDownloads() { return active.size() - innetworkCount - storeDownloadCount - mozillaDownloadCount; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getNumWaitingDownloads() */ public synchronized int getNumWaitingDownloads() { return waiting.size(); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getDownloaderForURN(com.limegroup.gnutella.URN) */ public synchronized Downloader getDownloaderForURN(URN sha1) { for (CoreDownloader md : activeAndWaiting) { if (md.getSha1Urn() != null && sha1.equals(md.getSha1Urn())) return md; } return null; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getDownloaderForURNString(java.lang.String) */ public synchronized Downloader getDownloaderForURNString(String urn) { for (CoreDownloader md : activeAndWaiting) { if (md.getSha1Urn() != null && urn.equals(md.getSha1Urn().toString())) return md; } return null; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getDownloaderForIncompleteFile(java.io.File) */ public synchronized Downloader getDownloaderForIncompleteFile(File file) { for (CoreDownloader dl : activeAndWaiting) { if (dl.conflictsWithIncompleteFile(file)) { return dl; } } return null; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#isGuidForQueryDownloading(com.limegroup.gnutella.GUID) */ public synchronized boolean isGuidForQueryDownloading(GUID guid) { for (CoreDownloader md : activeAndWaiting) { GUID dGUID = md.getQueryGUID(); if ((dGUID != null) && (dGUID.equals(guid))) return true; } return false; } void clearAllDownloads() { List<CoreDownloader> buf; synchronized(this) { buf = new ArrayList<CoreDownloader>(active.size() + waiting.size()); buf.addAll(active); buf.addAll(waiting); active.clear(); waiting.clear(); } for(CoreDownloader md : buf ) { md.stop(); fireEvent(md, DownloadManagerEvent.Type.REMOVED); } } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#download(com.limegroup.gnutella.RemoteFileDesc[], java.util.List, com.limegroup.gnutella.GUID, boolean, java.io.File, java.lang.String) */ public synchronized Downloader download(RemoteFileDesc[] files, List<? extends RemoteFileDesc> alts, GUID queryGUID, boolean overwrite, File saveDir, String fileName) throws DownloadException { String fName = getFileName(files, fileName); if (conflicts(files, new File(saveDir,fName))) { addRemoteFileDescsToDownloader(files); throw new DownloadException (ErrorCode.FILE_ALREADY_DOWNLOADING, new File(fName != null ? fName : "")); } //Purge entries from incompleteFileManager that have no corresponding //file on disk. This protects against stupid users who delete their //temporary files while LimeWire is running, either through the command //prompt or the library. Note that you could optimize this by just //purging files corresponding to the current download, but it's not //worth it. incompleteFileManager.purge(); //Start download asynchronously. This automatically moves downloader to //active if it can. ManagedDownloader downloader = coreDownloaderFactory.createManagedDownloader(files, queryGUID, saveDir, fileName, overwrite); initializeDownload(downloader, true); //Now that the download is started, add the sources w/o caching downloader.addDownload(alts,false); return downloader; } /** * Adds the provided file descs to the first downloader in the list that * matches up with it. */ private void addRemoteFileDescsToDownloader(RemoteFileDesc[] files) { List<CoreDownloader> downloaders = new ArrayList<CoreDownloader>(active.size() + waiting.size()); synchronized (this) { // add to all downloaders, even if they are waiting.... downloaders.addAll(active); downloaders.addAll(waiting); } for(CoreDownloader downloader : downloaders) { if(downloader instanceof ManagedDownloader) { ManagedDownloader managedDownloader = (ManagedDownloader) downloader; if(managedDownloader.addDownload(Arrays.asList(files), true)) { //only add fileDescs to 1 downloader break; } } } } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#download(com.limegroup.gnutella.browser.MagnetOptions, boolean, java.io.File, java.lang.String) */ public synchronized Downloader download(MagnetOptions magnet, boolean overwrite, File saveDir, String fileName) throws IllegalArgumentException, DownloadException { if (!magnet.isGnutellaDownloadable()) throw new IllegalArgumentException("magnet not downloadable"); //remove entry from IFM if the incomplete file was deleted. incompleteFileManager.purge(); if (fileName == null) { fileName = magnet.getFileNameForSaving(); } if (conflicts(magnet.getSHA1Urn(), 0, new File(saveDir,fileName))) { throw new DownloadException (ErrorCode.FILE_ALREADY_DOWNLOADING, new File(fileName)); } //Note: If the filename exists, it would be nice to check that we are //not already downloading the file by calling conflicts with the //filename...the problem is we cannot do this effectively without the //size of the file (at least, not without being risky in assuming that //two files with the same name are the same file). So for now we will //just leave it and download the same file twice. //Instantiate downloader, validating incompleteFile first. MagnetDownloader downloader = coreDownloaderFactory.createMagnetDownloader( magnet, overwrite, saveDir, fileName); initializeDownload(downloader, true); return downloader; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#download(java.io.File) */ public synchronized Downloader download(File incompleteFile) throws CantResumeException, DownloadException { if (conflictsWithIncompleteFile(incompleteFile)) { throw new DownloadException (ErrorCode.FILE_ALREADY_DOWNLOADING, incompleteFile); } if (IncompleteFileManager.isTorrentFile(incompleteFile)) { return resumeTorrentDownload(incompleteFile); } //Check if file exists. TODO3: ideally we'd pass ALL conflicting files //to the GUI, so they know what they're overwriting. //if (! overwrite) { // try { // File downloadDir=SettingsManager.instance().getSaveDirectory(); // File completeFile=new File( // downloadDir, // incompleteFileManager.getCompletedName(incompleteFile)); // if (completeFile.exists()) // throw new FileExistsException(filename); // } catch (IllegalArgumentException e) { // throw new CantResumeException(incompleteFile.getName()); // } //} //Purge entries from incompleteFileManager that have no corresponding //file on disk. This protects against stupid users who delete their //temporary files while LimeWire is running, either through the command //prompt or the library. Note that you could optimize this by just //purging files corresponding to the current download, but it's not //worth it. incompleteFileManager.purge(); //Instantiate downloader, validating incompleteFile first. ResumeDownloader downloader=null; try { incompleteFile = FileUtils.getCanonicalFile(incompleteFile); String name=IncompleteFileManager.getCompletedName(incompleteFile); long size= IncompleteFileManager.getCompletedSize(incompleteFile); downloader = coreDownloaderFactory.createResumeDownloader( incompleteFile, name, size); } catch (IllegalArgumentException e) { throw new CantResumeException(e, incompleteFile.getName()); } catch (IOException ioe) { throw new CantResumeException(ioe, incompleteFile.getName()); } initializeDownload(downloader, true); return downloader; } private Downloader resumeTorrentDownload(File torrentFile) throws CantResumeException, DownloadException { if(torrentManager.get().isValid()) { return downloadTorrent(torrentFile, null, false); } else { throw new CantResumeException("Torrent Manager Invalid", torrentFile.getName()); } } @Override public synchronized Downloader downloadTorrent(URI torrentURI, final boolean overwrite) throws DownloadException { if(!isSavedDownloadsLoaded()) { throw new DownloadException(DownloadException.ErrorCode.FILES_STILL_RESUMING, null); } if(!torrentManager.get().isValid()) { throw new DownloadException(DownloadException.ErrorCode.NO_TORRENT_MANAGER, null); } final BTTorrentFileDownloader torrentDownloader = coreDownloaderFactory .createTorrentFileDownloader(torrentURI, true); initializeDownload(torrentDownloader, false); return torrentDownloader; } @Override public synchronized Downloader downloadTorrent(File torrentFile, File saveDirectory, boolean overwrite) throws DownloadException { if(!isSavedDownloadsLoaded()) { throw new DownloadException(DownloadException.ErrorCode.FILES_STILL_RESUMING, torrentFile); } if(!torrentManager.get().isValid()) { throw new DownloadException(DownloadException.ErrorCode.NO_TORRENT_MANAGER, torrentFile); } if (torrentFile.length() > 1024 * 1024 * 5) { // torrent files are supposed to be small. If it is large it is // probably not a valid torrent file throw new DownloadException( DownloadException.ErrorCode.TORRENT_FILE_TOO_LARGE, torrentFile); } if(saveDirectory == null) { saveDirectory = SharingSettings.getSaveDirectory(); } TorrentParams params = new LibTorrentParams(SharingSettings.INCOMPLETE_DIRECTORY.get(), torrentFile); BTDownloader ret = null; try { params.fill(); checkIfAlreadyManagedTorrent(params); checkActiveAndWaiting(params, saveDirectory); ret = coreDownloaderFactory.createBTDownloader(params); if(ret == null || ret.getTorrent() == null || !ret.getTorrent().isValid()) { throw new DownloadException(DownloadException.ErrorCode.NO_TORRENT_MANAGER, torrentFile); } // Does the torrent contain any files with banned extensions? if(!overwrite) { Torrent torrent = ret.getTorrent(); if(!torrent.hasMetaData() || torrent.getTorrentInfo() == null) { if(LOG.isWarnEnabled()) LOG.warn("Torrent lacks info: " + torrentFile + ", size: " + torrentFile.length() + ", exists: " + torrentFile.exists()); } else { Set<String> banned = getBannedAndDisabledExtensions(torrent); if(!banned.isEmpty() && !downloadCallback.get().promptAboutTorrentWithBannedExtensions(torrent, banned)) throw new DownloadException(DownloadException.ErrorCode.DOWNLOAD_CANCELLED, torrentFile); } } ret.setSaveFile(saveDirectory, null, overwrite); if(!overwrite) { File saveFile = ret.getSaveFile(); if (saveFile.exists()) { throw new DownloadException(ErrorCode.FILE_ALREADY_EXISTS, saveFile); } } } catch (IOException e) { LOG.error("Error creating BTDownloader", e); if(ret != null) { Torrent torrent = ret.getTorrent(); torrentManager.get().removeTorrent(torrent); ret.deleteIncompleteFiles(); } if(e instanceof DownloadException) { throw (DownloadException)e; } else { throw new DownloadException(e, torrentFile); } } Torrent torrent = ret.getTorrent(); if(BittorrentSettings.TORRENT_SHOW_POPUP_BEFORE_DOWNLOADING.getValue() && !downloadCallback.get().promptTorrentFilePriorities(torrent)) { torrentManager.get().removeTorrent(torrent); ret.deleteIncompleteFiles(); throw new DownloadException(DownloadException.ErrorCode.DOWNLOAD_CANCELLED, torrentFile); } initializeDownload(ret, true); return ret; } //TODO: this may overwrite files in the library after metadata is found @Override public synchronized Downloader downloadTorrent(String name, URN sha1, List<URI> trackers) throws DownloadException { if(LOG.isInfoEnabled()) { LOG.info("Downloading torrent:"); LOG.info(" " + name); LOG.info(" " + sha1); for(URI tracker : trackers) { LOG.info(" " + tracker); } } if(!isSavedDownloadsLoaded()) { LOG.info("Saved downloads not loaded"); throw new DownloadException(DownloadException.ErrorCode.FILES_STILL_RESUMING, null); } if(!torrentManager.get().isValid()) { LOG.info("Torrent manager is not valid"); throw new DownloadException(DownloadException.ErrorCode.NO_TORRENT_MANAGER, null); } TorrentParams params = new LibTorrentParams(SharingSettings.INCOMPLETE_DIRECTORY.get(), name, StringUtils.toHexString(sha1.getBytes()), trackers); checkIfAlreadyManagedTorrent(params); try { final BTDownloader torrentDownloader = coreDownloaderFactory.createBTDownloader(params); initializeDownload(torrentDownloader, false); return torrentDownloader; } catch (IOException e) { LOG.error("Error creating BTDownloader", e); if(e instanceof DownloadException) { throw (DownloadException)e; } else { throw new DownloadException(e, null); } } } /** * Returns a (possibly empty) set of banned or disabled file extensions * belonging to files in the given torrent. This method should only be * called for torrents with metadata and torrent info. * Package access for testing. */ Set<String> getBannedAndDisabledExtensions(Torrent torrent) { assert torrent.hasMetaData(); TorrentInfo info = torrent.getTorrentInfo(); assert info != null; Set<String> extensions = new HashSet<String>(); for(TorrentFileEntry entry: info.getTorrentFileEntries()) { String ext = FileUtils.getFileExtension(entry.getPath()); if(!ext.isEmpty()) extensions.add(ext); } Set<String> banned = new HashSet<String>(); for(String extWithDot : FilterSettings.BANNED_EXTENSIONS.get()) { // Sanity check in case the user did something weird to the setting if(extWithDot.length() > 1 && extWithDot.startsWith(".")) banned.add(extWithDot.substring(1)); } if(!LibrarySettings.ALLOW_PROGRAMS.getValue()) { banned.addAll(categoryManager.getExtensionsForCategory(Category.PROGRAM)); } extensions.retainAll(banned); return extensions; } /** * Ensures the eventual download location is not already taken by the files * of any other download. */ private void checkActiveAndWaiting(TorrentParams params, File saveDirectory) throws DownloadException { URN urn = null; try { urn = URN.createSha1UrnFromHex(params.getSha1()); } catch (IOException e) { throw new DownloadException(ErrorCode.FILESYSTEM_ERROR, params.getTorrentFile()); } for (CoreDownloader current : activeAndWaiting) { if (urn.equals(current.getSha1Urn())) { throw new DownloadException(ErrorCode.FILE_ALREADY_DOWNLOADING, params.getTorrentDataFile()); } File saveFile = new File(saveDirectory, params.getName()); if (current.conflictsSaveFile(saveFile)) { throw new DownloadException(ErrorCode.FILE_IS_ALREADY_DOWNLOADED_TO, saveFile); } if (current.conflictsSaveFile(params.getTorrentDataFile())) { throw new DownloadException(ErrorCode.FILE_ALREADY_DOWNLOADING, params .getTorrentDataFile()); } } } private void checkIfAlreadyManagedTorrent(TorrentParams params) throws DownloadException { Torrent torrent = torrentManager.get().getTorrent(params.getTorrentFile()); if(torrent == null) { torrent = torrentManager.get().getTorrent(params.getSha1()); } if(torrent != null) { if(!torrent.isFinished()) { LOG.info("Already downloading"); throw new DownloadException(ErrorCode.FILE_ALREADY_DOWNLOADING, params.getTorrentDataFile()); } else { LOG.info("Already uploading"); throw new DownloadException(ErrorCode.FILE_ALREADY_UPLOADING, params.getTorrentDataFile()); } } } public synchronized Downloader downloadFromMozilla(MozillaDownload listener) { CoreDownloader downloader = new MozillaDownloaderImpl(this, categoryManager, listener); downloader.initialize(); callback(downloader).addDownload(downloader); active.add(downloader); mozillaDownloadCount++; fireEvent(downloader, DownloadManagerEvent.Type.ADDED); return downloader; } /** * Performs common tasks for initializing the download. * 1) Initializes the downloader. * 2) Adds the download to the waiting list. * 3) Notifies the callback about the new downloader. * 4) Writes the new snapshot out to disk. */ private synchronized void initializeDownload(final CoreDownloader md, boolean saveState) { md.initialize(); waiting.add(md); callback(md).addDownload(md); if(saveState) { backgroundExecutor.execute(new Runnable() { public void run() { writeSnapshot(); // Save state for crash recovery. } }); } // TODO: do this outside the lock fireEvent(md, DownloadManagerEvent.Type.ADDED); } /** * Returns the callback that should be used for the given md. */ private DownloadCallback callback(Downloader md) { return downloadCallback.get(); } /** * Returns true if there already exists a download for the same file. * <p> * Same file means: same urn, or as fallback same filename + same filesize */ private boolean conflicts(RemoteFileDesc[] rfds, File... fileName) { URN urn = null; for (int i = 0; i < rfds.length && urn == null; i++) { urn = rfds[0].getSHA1Urn(); } return conflicts(urn, rfds[0].getSize(), fileName); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#conflicts(com.limegroup.gnutella.URN, long, java.io.File) */ public boolean conflicts(URN urn, long fileSize, File... fileName) { if (urn == null && fileSize == 0) { return false; } synchronized (this) { for (CoreDownloader md : activeAndWaiting) { if (md.conflicts(urn, fileSize, fileName)) return true; } return false; } } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#isSaveLocationTaken(java.io.File) */ public synchronized boolean isSaveLocationTaken(File candidateFile) { for (CoreDownloader md : activeAndWaiting) { if (md.conflictsSaveFile(candidateFile)) return true; } return false; } private synchronized boolean conflictsWithIncompleteFile(File incompleteFile) { for (CoreDownloader md : activeAndWaiting) { if (md.conflictsWithIncompleteFile(incompleteFile)) return true; } return false; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#handleQueryReply(com.limegroup.gnutella.messages.QueryReply) */ public void handleQueryReply(QueryReply qr, Address address) { // first check if the qr is of 'sufficient quality', if not just // short-circuit. if (qr.calculateQualityOfService() < 1) return; List<Response> responses; try { qr.validate(); responses = qr.getResultsAsList(); } catch(BadPacketException bpe) { return; // bad packet, do nothing. } addDownloadWithResponses(responses, qr, address); } /** * Iterates through all responses seeing if they can be matched * up to any existing downloaders, adding them as possible * sources if they do. * @param address can be null */ private void addDownloadWithResponses(List<? extends Response> responses, QueryReply queryReply, Address address) { if(responses == null) throw new NullPointerException("null responses"); if(queryReply == null) throw new NullPointerException("null queryReply"); // need to synch because active and waiting are not thread safe List<CoreDownloader> downloaders = new ArrayList<CoreDownloader>(active.size() + waiting.size()); synchronized (this) { // add to all downloaders, even if they are waiting.... downloaders.addAll(active); downloaders.addAll(waiting); } // short-circuit. if(downloaders.isEmpty()) return; //For each response i, offer it to each downloader j. Give a response // to at most one downloader. // TODO: it's possible that downloader x could accept response[i] but //that would cause a conflict with downloader y. Check for this. for(Response r : responses) { // Don't bother with making XML from the EQHD. RemoteFileDesc rfd; try { rfd = r.toRemoteFileDesc(queryReply, address, remoteFileDescFactory, pushEndpointFactory); } catch (UnknownHostException e) { throw new RuntimeException(e); } for(Downloader current : downloaders) { if ( !(current instanceof ManagedDownloader)) continue; // can't add sources to torrents yet ManagedDownloader currD = (ManagedDownloader) current; // If we were able to add this specific rfd, // add any alternates that this response might have // also. LOG.debugf("adding rfd {0} to downloader {1}", rfd, currD); if (currD.addDownload(rfd, true)) { for(IpPort ipp : r.getLocations()) { // don't cache alts. currD.addDownload(remoteFileDescFactory.createRemoteFileDesc(rfd, ipp), false); } break; } } } } // //////////// Callback Methods for ManagedDownloaders /////////////////// /** @requires this monitor' held by caller */ private boolean hasFreeSlot() { return active.size() - innetworkCount - storeDownloadCount - mozillaDownloadCount < DownloadSettings.MAX_SIM_DOWNLOAD.getValue(); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#remove(com.limegroup.gnutella.downloader.CoreDownloader, boolean) */ public synchronized void remove(CoreDownloader downloader, boolean completed) { if(active.remove(downloader)) { DownloaderType type = downloader.getDownloadType(); // These counters only apply to active downloads if(type == DownloaderType.INNETWORK) innetworkCount--; else if(type == DownloaderType.STORE) storeDownloadCount--; else if(type == DownloaderType.MOZILLA) mozillaDownloadCount--; } waiting.remove(downloader); if(completed) cleanupCompletedDownload(downloader, true); else waiting.add(downloader); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#bumpPriority(com.limegroup.gnutella.Downloader, boolean, int) */ public synchronized void bumpPriority(Downloader downl, boolean up, int amt) { CoreDownloader downloader = (CoreDownloader)downl; int idx = waiting.indexOf(downloader); if(idx == -1) return; if(up && idx != 0) { waiting.remove(idx); if (amt > idx) amt = idx; if (amt != 0) waiting.add(idx - amt, downloader); else waiting.add(0, downloader); //move to top of list } else if(!up && idx != waiting.size() - 1) { waiting.remove(idx); if (amt != 0) { amt += idx; if (amt > waiting.size()) amt = waiting.size(); waiting.add(amt, downloader); } else { waiting.add(downloader); //move to bottom of list } } } /** * Cleans up the given Downloader after completion. * * If ser is true, also writes a snapshot to the disk. */ private void cleanupCompletedDownload(CoreDownloader dl, boolean ser) { dl.finish(); if (dl.getQueryGUID() != null) messageRouter.get().downloadFinished(dl.getQueryGUID()); callback(dl).removeDownload(dl); //Save this' state to disk for crash recovery. if(ser) writeSnapshot(); // Enable auto shutdown if(active.isEmpty() && waiting.isEmpty()) callback(dl).downloadsComplete(); fireEvent(dl, DownloadManagerEvent.Type.REMOVED); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#sendQuery(com.limegroup.gnutella.downloader.ManagedDownloader, com.limegroup.gnutella.messages.QueryRequest) */ public void sendQuery(QueryRequest query) { messageRouter.get().sendDynamicQuery(query); } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#measureBandwidth() */ public void measureBandwidth() { List<CoreDownloader> activeCopy; synchronized(this) { activeCopy = new ArrayList<CoreDownloader>(active); } float currentTotal = 0f; boolean c = false; for (BandwidthTracker bt : activeCopy) { c = true; bt.measureBandwidth(); currentTotal += bt.getAverageBandwidth(); } if ( c ) { synchronized(this) { averageBandwidth = ( (averageBandwidth * numMeasures) + currentTotal ) / ++numMeasures; } } } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getMeasuredBandwidth() */ public float getMeasuredBandwidth() { List<CoreDownloader> activeCopy; synchronized(this) { activeCopy = new ArrayList<CoreDownloader>(active); } float sum=0; for (BandwidthTracker bt : activeCopy) { float curr = 0; try{ curr = bt.getMeasuredBandwidth(); } catch(InsufficientDataException ide) { curr = 0;//insufficient data? assume 0 } sum+=curr; } lastMeasuredBandwidth = sum; return sum; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getAverageBandwidth() */ public synchronized float getAverageBandwidth() { return averageBandwidth; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getLastMeasuredBandwidth() */ public float getLastMeasuredBandwidth() { return lastMeasuredBandwidth; } private String getFileName(RemoteFileDesc[] rfds, String fileName) { for (int i = 0; i < rfds.length && fileName == null; i++) { fileName = rfds[i].getFileName(); } return fileName; } /* (non-Javadoc) * @see com.limegroup.gnutella.DownloadManager#getAllDownloaders() */ public final Iterable<CoreDownloader> getAllDownloaders() { return activeAndWaiting; } /** * Listens for events from FileManager */ public void handleEvent(LibraryStatusEvent evt) { switch(evt.getType()){ case LOAD_FINISHING: getIncompleteFileManager().registerAllIncompleteFiles(); break; } } // --------------------------------------------------------------- // Implementation of LWSIntegrationServicesDelegate public synchronized void visitDownloads(Visitor<CoreDownloader> visitor) { for (CoreDownloader downloader : activeAndWaiting) { visitor.visit(downloader); } } private void fireEvent(CoreDownloader downloader, DownloadManagerEvent.Type type) { listeners.broadcast(new DownloadManagerEvent(downloader, type)); } public void addListener(EventListener<DownloadManagerEvent> listener) { listeners.addListener(listener); } public boolean removeListener(EventListener<DownloadManagerEvent> listener) { return listeners.removeListener(listener); } @Override public synchronized boolean contains(Downloader downloader) { for(CoreDownloader coreDownloader : activeAndWaiting) { if(coreDownloader == downloader) { return true; } } return false; } }