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.ArrayList; import java.util.Arrays; import java.util.Collections; 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.TorrentEventType; 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.TorrentState; import org.limewire.bittorrent.TorrentStatus; import org.limewire.bittorrent.util.TorrentUtil; import org.limewire.core.api.download.DownloadPiecesInfo; import org.limewire.core.api.download.SaveLocationManager; import org.limewire.core.api.file.CategoryManager; import org.limewire.core.api.malware.VirusEngine; import org.limewire.core.api.transfer.SourceInfo; import org.limewire.core.settings.BittorrentSettings; import org.limewire.core.settings.SharingSettings; 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.libtorrent.LibTorrentParams; 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; import com.limegroup.gnutella.malware.VirusScanException; import com.limegroup.gnutella.malware.VirusScanner; /** * 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 AtomicInteger torrentsStarted = new AtomicInteger(); private static final AtomicInteger torrentsFinished = new AtomicInteger(); private final DownloadManager downloadManager; private volatile 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<TorrentManager> torrentManager; private final Provider<TorrentUploadManager> torrentUploadManager; private final Provider<DangerousFileChecker> dangerousFileChecker; private final Provider<VirusScanner> virusScanner; private final Provider<DownloadCallback> downloadCallback; /** * Whether a preview that could not be scanned for viruses should be * deleted. */ private volatile boolean discardUnscannedPreview; /** * Torrent info hash based URN used as a cache for getSha1Urn(). */ private volatile URN urn = null; @Inject BTDownloaderImpl(SaveLocationManager saveLocationManager, DownloadManager downloadManager, BTUploaderFactory btUploaderFactory, Library library, @Named("fastExecutor") ScheduledExecutorService fastExecutor, @GnutellaFiles FileCollection gnutellaFileCollection, Provider<TorrentManager> torrentManager, Provider<TorrentUploadManager> torrentUploadManager, Provider<DangerousFileChecker> dangerousFileChecker, Provider<VirusScanner> virusScanner, Provider<DownloadCallback> downloadCallback, CategoryManager categoryManager) { super(saveLocationManager, categoryManager); this.downloadManager = downloadManager; this.btUploaderFactory = btUploaderFactory; this.library = library; this.gnutellaFileCollection = gnutellaFileCollection; this.listeners = new AsynchronousMulticasterImpl<DownloadStateEvent>(fastExecutor); this.torrentManager = torrentManager; this.torrentUploadManager = torrentUploadManager; this.dangerousFileChecker = dangerousFileChecker; this.virusScanner = virusScanner; this.downloadCallback = downloadCallback; discardUnscannedPreview = true; } @Override public void handleEvent(TorrentEvent event) { if (TorrentEventType.COMPLETED == event.getType() && !complete.get()) { LOG.debug("Finished"); finishing.set(true); torrentsFinished.incrementAndGet(); if (isInfectedOrDangerous()) { return; } FileUtils.forceDeleteRecursive(getSaveFile()); File completeDir = getSaveFile().getParentFile(); torrent.getLock().lock(); try { torrent.moveTorrent(completeDir); File torrentUploadFolder = BittorrentSettings.TORRENT_UPLOADS_FOLDER.get(); File oldTorrentFile = torrent.getTorrentFile(); if (oldTorrentFile != null) { File newTorrentFile = new File(torrentUploadFolder, oldTorrentFile.getName()); torrent.setTorrentFile(newTorrentFile); FileUtils.copy(oldTorrentFile, newTorrentFile); FileUtils.forceDelete(oldTorrentFile); } File oldFastResumeFile = torrent.getFastResumeFile(); if (oldFastResumeFile != null) { File newFastResumeFile = new File(torrentUploadFolder, oldFastResumeFile.getName()); torrent.setFastResumeFile(newFastResumeFile); FileUtils.copy(oldFastResumeFile, newFastResumeFile); FileUtils.forceDelete(oldFastResumeFile); } } finally { torrent.getLock().unlock(); } createUploadMemento(); cleanupPriorityZeroFiles(); File completeFile = getSaveFile(); addFileToCollections(completeFile); complete.set(true); deleteIncompleteFiles(); if(lastState.get() != DownloadState.SCAN_FAILED) { lastState.set(DownloadState.COMPLETE); listeners.broadcast(new DownloadStateEvent(this, DownloadState.COMPLETE)); } BTDownloaderImpl.this.downloadManager.remove(BTDownloaderImpl.this, true); torrent.removeListener(BTDownloaderImpl.this); } else if (TorrentEventType.STOPPED == event.getType()) { torrent.removeListener(this); // Was the torrent stopped because of a virus or dangerous file? if (lastState.get() != DownloadState.DANGEROUS && lastState.get() != DownloadState.THREAT_FOUND) { lastState.set(DownloadState.ABORTED); listeners.broadcast(new DownloadStateEvent(this, DownloadState.ABORTED)); } BTDownloaderImpl.this.downloadManager.remove(BTDownloaderImpl.this, true); deleteIncompleteFiles(); } else if (TorrentEventType.FAST_RESUME_FILE_SAVED == event.getType()) { // nothing to do now. } else if (TorrentEventType.STARTED == event.getType()) { torrentsStarted.incrementAndGet(); } else if (TorrentEventType.META_DATA_RECEIVED == event.getType() && getTorrentFile() == null) { // Hack to either cancel the torrent incase of collision or // fix the save path after metadata is received in a torrent // file-less download if (getSaveFile().exists()) { stop(); } else { String newName = event.getTorrent().getName(); setSaveFileInternal(new File(getSaveFile().getParentFile(), newName)); setDefaultFileName(newName); } } 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 infected or dangerous files in this * torrent, after stopping the download. */ private boolean isInfectedOrDangerous() { if(virusScanner.get().isEnabled()) { lastState.set(DownloadState.SCANNING); listeners.broadcast(new DownloadStateEvent(this, DownloadState.SCANNING)); try { if(isInfected(getIncompleteFile())) return true; } catch(VirusScanException e) { LOG.error("Error scanning file", e); setAttribute(VirusEngine.DOWNLOAD_FAILURE_HINT, e.getDetail(), false); lastState.set(DownloadState.SCAN_FAILED); listeners.broadcast(new DownloadStateEvent(this, lastState.get())); } } for(File f : getIncompleteFiles()) { if(isDangerous(f)) return true; } return false; } /** * Checks whether a file fragment is infected or dangerous. If the virus * scan fails, the user will be asked whether to preview the file anyway. * @param fragment the file to check * @param listener a listener to be informed of virus scan progress * @return true if the file cannot be previewed. */ private boolean isInfectedOrDangerous(File fragment, ScanListener listener) { if(virusScanner.get().isEnabled()) { LOG.debug("Starting preview scan"); listener.scanStarted(); try { boolean infected = isInfected(fragment); listener.scanStopped(); if(infected) return true; } catch(VirusScanException e) { LOG.error("Error scanning file", e); listener.scanStopped(); if(promptAboutUnscannedPreview()) { // The user chose to cancel the preview LOG.debug("User chose to cancel preview"); return true; } LOG.debug("User chose to continue with preview"); } } return isDangerous(fragment); } /** * Returns true if the given file is infected, after stopping the download. */ private boolean isInfected(File file) throws VirusScanException { if(LOG.isDebugEnabled()) LOG.debug("Scanning " + file); if(virusScanner.get().isInfected(file)) { if(LOG.isDebugEnabled()) LOG.debug(file + " is infected"); lastState.set(DownloadState.THREAT_FOUND); listeners.broadcast(new DownloadStateEvent(this, DownloadState.THREAT_FOUND)); // This will cause TorrentEvent.STOPPED torrent.stop(); return true; } return false; } /** * Returns true if the given file is dangerous, after stopping the download. */ private boolean isDangerous(File file) { if(LOG.isDebugEnabled()) LOG.debug("Checking whether " + file + " is dangerous"); if (dangerousFileChecker.get().isDangerous(file)) { if(LOG.isDebugEnabled()) LOG.debug(file + " is dangerous"); lastState.set(DownloadState.DANGEROUS); listeners.broadcast(new DownloadStateEvent(this, DownloadState.DANGEROUS)); // This will cause TorrentEvent.STOPPED torrent.stop(); return true; } return false; } private boolean promptAboutUnscannedPreview() { downloadCallback.get().promptAboutUnscannedPreview(this); return discardUnscannedPreview; } @Override public void discardUnscannedPreview(boolean delete) { discardUnscannedPreview = delete; } /** * Checks to see if this torrent has any priority zero files and removes * them. */ private void cleanupPriorityZeroFiles() { LOG.debug("Cleaning up zero priority files"); boolean hasAnyPriorityZero = false; List<TorrentFileEntry> fileEntries = torrent.getTorrentFileEntries(); for (TorrentFileEntry fileEntry : fileEntries) { if (fileEntry.getPriority() == 0) { hasAnyPriorityZero = true; break; } } if (hasAnyPriorityZero) { for (TorrentFileEntry fileEntry : fileEntries) { if (fileEntry.getPriority() == 0) { File torrentDataFile = torrent.getTorrentDataFile(fileEntry); if(LOG.isDebugEnabled()) LOG.debug("Deleting " + torrentDataFile); FileUtils.forceDelete(torrentDataFile); } } FileUtils.deleteEmptyDirectories(getSaveFile()); } } /** * 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()) { if(LOG.isDebugEnabled()) LOG.debug("Adding directory " + completeFile + " to library"); 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 { if(LOG.isDebugEnabled()) LOG.debug("Adding file " + completeFile + " to library"); library.add(completeFile); if (!torrent.isPrivate() && SharingSettings.SHARE_DOWNLOADED_FILES_IN_NON_SHARED_DIRECTORIES.getValue()) { gnutellaFileCollection.add(completeFile); } } }; @Override public void init(TorrentParams params) throws IOException { LOG.debug("Initializing"); this.torrent = torrentManager.get().addTorrent(params); if(torrent == null) { LOG.debug("Error adding torrent to TorrentManager"); throw new IOException("Error adding torrent to TorrentManager."); } torrent.addListener(this); setDefaultFileName(torrent.getName()); } /** * 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()) { LOG.debug("Stopping when unfinished"); torrent.stop(); downloadManager.remove(this, true); } else { LOG.debug("Stopping when finished"); downloadManager.remove(this, true); } } @Override public void pause() { LOG.debug("Pausing"); torrent.pause(); } @Override public boolean isPaused() { return torrent.isPaused(); } @Override public boolean isLaunchable() { if (isCompleted()) { LOG.debug("Launchable: completed"); return true; } TorrentInfo torrentInfo = torrent.getTorrentInfo(); if (torrentInfo == null || torrentInfo.getTorrentFileEntries().size() > 1) { LOG.debug("Not launchable: no torrent info or multiple files"); return false; } LOG.debug("Launchable: torrent info and only one file"); return true; } @Override public boolean resume() { LOG.debug("Resuming"); torrent.resume(); return true; } @Override public File getFile() { if (torrent.isFinished()) { LOG.debug("Finished: returning save file"); return getSaveFile(); } else { LOG.debug("Unfinished: returning incomplete file"); return getIncompleteFile(); } } @Override public File getIncompleteFile() { return new File(SharingSettings.INCOMPLETE_DIRECTORY.get(), torrent.getName()); } @Override public File getDownloadFragment(ScanListener listener) { if (isCompleted()) { LOG.debug("Returning complete file"); return getSaveFile(); } // Can't preview a multi-file download TorrentInfo torrentInfo = torrent.getTorrentInfo(); if (torrentInfo == null || torrentInfo.getTorrentFileEntries().size() > 1) { LOG.debug("No torrent info or multiple files"); return null; } // Return a copy of the completed part of the file File copy = 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, copy) <= 0) { LOG.debug("Failed to create preview copy"); return null; } if (isInfectedOrDangerous(copy, listener)) { LOG.debug("Not returning infected or dangerous preview copy"); copy.delete(); return null; } LOG.debug("Returning preview copy"); return copy; } @Override public DownloadState getState() { switch(lastState.get()) { case DANGEROUS: return DownloadState.DANGEROUS; case THREAT_FOUND: return DownloadState.THREAT_FOUND; case SCAN_FAILED: return DownloadState.SCAN_FAILED; case SCANNING: return DownloadState.SCANNING; } 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()) { if(LOG.isErrorEnabled()) LOG.error("Error downloading torrent: " + status.getError()); // 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; if(LOG.isDebugEnabled()) LOG.debug("Content length: " + contentLength); return contentLength; } @Override public long getAmountRead() { TorrentStatus status = torrent.getStatus(); if (status == null) { LOG.debug("No status: cannot get amount read"); return -1; } else { long amountRead = status.getTotalWantedDone(); if(LOG.isDebugEnabled()) LOG.debug("Amount read: " + amountRead); return amountRead; } } @Override public long getAmountVerified() { TorrentStatus status = torrent.getStatus(); if (status == null) { LOG.debug("No status: cannot get amount verified"); return -1; } else { long amountVerified = status.getTotalWantedDone(); if(LOG.isDebugEnabled()) LOG.debug("Amount verified: " + amountVerified); return amountVerified; } } @Override public long getAmountLost() { TorrentStatus status = torrent.getStatus(); if (status == null) { LOG.debug("No status: cannot get amount lost"); return -1; } else { long amountLost = status.getTotalFailedDownload(); if(LOG.isDebugEnabled()) LOG.debug("Amount lost: " + amountLost); return amountLost; } } @Override public List<RemoteFileDesc> getRemoteFileDescs() { return Collections.emptyList(); } @Override public int getQueuePosition() { return 1; } @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: case DANGEROUS: case THREAT_FOUND: case SCAN_FAILED: LOG.debug("Should be removed"); return true; } LOG.debug("Should not be removed"); return false; } @Override public void measureBandwidth() { // Unused, we are using the bandwidth reported by libtorrent } @Override public float getMeasuredBandwidth() throws InsufficientDataException { float bw = torrent.isPaused() ? 0 : (torrent.getDownloadRate() / 1024); if(LOG.isDebugEnabled()) LOG.debug("Measured bandwidth: " + bw); return bw; } @Override public float getAverageBandwidth() { // Unused by anything float bw = torrent.isPaused() ? 0 : (torrent.getDownloadRate() / 1024); if(LOG.isDebugEnabled()) LOG.debug("Average bandwidth: " + bw); return bw; } @Override public boolean isRelocatable() { return !isCompleted(); } @Override protected File getDefaultSaveFile() { File f = new File(SharingSettings.getSaveDirectory(), torrent.getName()); if(LOG.isDebugEnabled()) LOG.debug("Default save file: " + f); return f; } @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); } } } } LOG.debug(urn); return urn; } @Override public int getNumHosts() { int hosts = torrent.getNumPeers(); if(LOG.isDebugEnabled()) LOG.debug(hosts + " hosts"); return hosts; } @Override public List<Address> getSourcesAsAddresses() { List<TorrentPeer> peers = torrent.getTorrentPeers(); List<Address> list = new ArrayList<Address>(peers.size()); for (TorrentPeer peer : peers) { String ip = peer.getIPAddress(); if (ip != null) { try { list.add(new ConnectableImpl(new IpPortImpl(ip), false)); if(LOG.isDebugEnabled()) LOG.debug("Peer: " + ip); } catch (UnknownHostException e) { // Discard invalid host if(LOG.isDebugEnabled()) LOG.debug("Invalid peer: " + ip); } } } return list; } @Override public List<SourceInfo> getSourcesDetails() { List<TorrentPeer> peers = torrent.getTorrentPeers(); List<SourceInfo> sourceInfoList = new ArrayList<SourceInfo>(peers.size()); for (TorrentPeer peer : peers) { sourceInfoList.add(new TorrentSourceInfoAdapter(peer)); } return sourceInfoList; } @Override public DownloadPiecesInfo getPieceInfo() { return new BTDownloadPiecesInfo(torrent); } @Override public void initialize() { } @Override public void startDownload() { LOG.debug("Starting download"); 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() { LOG.debug("Finishing"); deleteIncompleteFiles(); } @Override public DownloaderType getDownloadType() { return DownloaderType.BTDOWNLOADER; } @Override protected DownloadMemento createMemento() { LOG.debug("Creating memento"); return new LibTorrentBTDownloadMementoImpl(); } @Override protected void fillInMemento(DownloadMemento memento) { LOG.debug("Filling in memento"); super.fillInMemento(memento); LibTorrentBTDownloadMemento btMemento = (LibTorrentBTDownloadMemento) memento; btMemento.setName(torrent.getName()); btMemento.setSha1Urn(getSha1Urn()); btMemento.setIncompleteFile(getIncompleteFile()); btMemento.setTrackers(torrent.getTrackerURIS()); 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 { LOG.debug("Initializing from memento"); urn = memento.getSha1Urn(); if (urn == null) { LOG.debug("Null URN"); throw new InvalidDataException( "Null SHA1 URN retrieved from LibTorrent torrent momento."); } if (!urn.isSHA1()) { LOG.debug("Non-SHA1 URN"); 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 LibTorrentParams(SharingSettings.INCOMPLETE_DIRECTORY.get(), memento.getName(), StringUtils.toHexString(urn.getBytes())); params.setTrackers(memento.getTrackers()); params.setFastResumeFile(fastResumeFile); params.setTorrentFile(torrentFile); params.setTorrentDataFile(memento.getIncompleteFile()); params.setPrivate(memento.isPrivate()); init(params); } catch (IOException e) { LOG.debug("Could not initialize downloader (first try)", e); // the .torrent file could be invalid, try to initialize just with // the memento contents. try { TorrentParams params = new LibTorrentParams( SharingSettings.INCOMPLETE_DIRECTORY.get(), memento.getName(), StringUtils.toHexString(urn.getBytes())); params.setTrackers(memento.getTrackers()); params.setFastResumeFile(fastResumeFile); params.setTorrentDataFile(memento.getIncompleteFile()); params.setPrivate(memento.isPrivate()); init(params); } catch (IOException e1) { LOG.debug("Could not initialize downloader (second try)", e1); throw new InvalidDataException("Could not initialize the BTDownloader", e1); } } } public void initFromOldMemento(BTDownloadMemento memento) throws InvalidDataException { LOG.debug("Initializing from old memento"); BTMetaInfoMemento btmetainfo = memento.getBtMetaInfoMemento(); URI[] trackers = btmetainfo.getTrackers(); String name = btmetainfo.getFileSystem().getName(); byte[] infoHash = btmetainfo.getInfoHash(); URN sha1; try { sha1 = URN.createSHA1UrnFromBytes(infoHash); } catch (IOException e) { LOG.debug("Could not create URN", e); throw new InvalidDataException( "Could not initialize the BTDownloader, memento hash was invalid", e); } 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()) { LOG.debug("Incomplete file already 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 LibTorrentParams(newIncompleteFile.getParentFile(), name, StringUtils.toHexString(sha1.getBytes())); params.setTrackers(Arrays.asList(trackers)); params.setTorrentDataFile(newIncompleteFile); params.setPrivate(isPrivate); init(params); } catch (IOException e) { LOG.debug("Could not initialize downloader", 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 (!torrent.isValid()) { LOG.debug("Error registering torrent"); 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() { LOG.debug("Deleting incomplete files"); if (!complete.get()) { LOG.debug("Not complete"); File incompleteFile = getIncompleteFile(); if (incompleteFile != null) { LOG.debug("Deleting incomplete file"); FileUtils.forceDeleteRecursive(incompleteFile); } } if(torrent.getTorrentFile() != null && torrent.getTorrentFile().getParentFile().equals(SharingSettings.INCOMPLETE_DIRECTORY.get())) { LOG.debug("Deleting torrent file"); FileUtils.forceDelete(torrent.getTorrentFile()); } if(torrent.getFastResumeFile().getParentFile().equals(SharingSettings.INCOMPLETE_DIRECTORY.get())) { LOG.debug("Deleting fast resume file"); FileUtils.forceDelete(torrent.getFastResumeFile()); } } @Override public List<File> getCompleteFiles() { List<File> completeFiles = TorrentUtil.buildTorrentFiles(torrent, getSaveFile().getParentFile()); if(LOG.isDebugEnabled()) { LOG.debug("Getting complete files:"); for(File f : completeFiles) { LOG.debug(" " + f); } } return completeFiles; } public List<File> getIncompleteFiles() { List<File> incompleteFiles = TorrentUtil.buildTorrentFiles(torrent, getIncompleteFile().getParentFile()); if(LOG.isDebugEnabled()) { LOG.debug("Getting incomplete files:"); for(File f : incompleteFiles) { LOG.debug(" " + f); } } return incompleteFiles; } @Override public boolean conflicts(URN urn, long fileSize, File... file) { if (getSha1Urn().equals(urn)) { LOG.debug("Conflicts with URN"); return true; } for (File f : file) { if (conflictsSaveFile(f)) { return true; } } LOG.debug("Does not conflict"); return false; } @Override public boolean conflictsSaveFile(File complete) { boolean conflicts = complete.equals(getSaveFile()); if(conflicts) LOG.debug("Conflicts with save file"); return conflicts; } @Override public boolean conflictsWithIncompleteFile(File incomplete) { boolean conflicts = incomplete.equals(getIncompleteFile()); if(conflicts) LOG.debug("Conflicts with incomplete file"); return conflicts; } /** * 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 int getAmountPending() { throw new UnsupportedOperationException( "BTDownloaderImpl.getAmountPending() not implemented"); } @Override public File getTorrentFile() { File torrentFile = torrent.getTorrentFile(); if(LOG.isDebugEnabled()) LOG.debug("Getting torrent file: " + torrentFile); return torrentFile; } public Torrent getTorrent() { return torrent; } }