package net.flibusta.download.impl; import net.flibusta.download.DownloadException; import net.flibusta.download.DownloadService; import net.flibusta.util.TempFileUtil; import org.apache.commons.httpclient.*; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.params.HttpConnectionManagerParams; import org.apache.commons.httpclient.params.HttpMethodParams; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import java.io.*; import java.net.URL; import java.util.concurrent.*; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class TimedDownloadService implements DownloadService { Logger logger = Logger.getLogger(TimedDownloadService.class); private ExecutorService fetchExecutor; private int fetchPoolSize = 2; private long fetchTimeoutSeconds = 60; private String onionProxyHost; private int onionProxyPort; public void setOnionProxyHost(String onionProxyHost) { this.onionProxyHost = onionProxyHost; } public void setOnionProxyPort(int onionProxyPort) { this.onionProxyPort = onionProxyPort; } public void setI2pProxyHost(String i2pProxyHost) { this.i2pProxyHost = i2pProxyHost; } public void setI2pProxyPort(int i2pProxyPort) { this.i2pProxyPort = i2pProxyPort; } private String i2pProxyHost; private int i2pProxyPort; public TimedDownloadService() { } public void init() { fetchExecutor = Executors.newFixedThreadPool(fetchPoolSize); } @Override public File fetch(final URL url) throws Exception { logger.debug("Fetch " + url); Callable<File> task = new Callable<File>() { @Override public File call() throws Exception { return executeFetch(url); } }; Future<File> submit; try { submit = fetchExecutor.submit(task); } catch (RejectedExecutionException e) { logger.warn("Fetch Pool overloaded. url=" + url); throw new RejectedExecutionException("Server overloaded. Please try late."); } try { return submit.get(fetchTimeoutSeconds, TimeUnit.SECONDS); } catch (TimeoutException e) { throw new TimeoutException("Book download timed out for " + url); } } public File executeFetch(URL url) throws Exception { HttpClient httpClient = getHttpClient(); HostConfiguration hostConfiguration = getHostConfiguration(url); String uri = url.toString(); GetMethod method = new GetMethod(uri); method.getParams().setParameter(HttpMethodParams.USER_AGENT, "Mobipocket/ePub Converter"); // method.getParams().setParameter(HttpMethodParams.USER_AGENT, "Mozilla/5.0 (X11; Linux i686) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.46 Safari/536.5"); method.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, (int) TimeUnit.MILLISECONDS.convert(fetchTimeoutSeconds, TimeUnit.SECONDS)); int retryCount = 10; int code; String fileName; File file; try { try { do { logger.debug("Start download " + url + " try=" + (10 - retryCount)); code = httpClient.executeMethod(hostConfiguration, method); if (code == 503) { Thread.sleep(500); } } while (code == 503 && retryCount-- > 0); } catch (IOException e) { logger.error("Download from " + uri + " failed with exception " + e.getMessage(), e); method.releaseConnection(); throw new DownloadException("Download from " + uri + " failed with exception " + e.getMessage(), e); } if (code != 200) { method.releaseConnection(); throw new DownloadException("File download failed with code " + code + " from url " + uri); } InputStream sourceStream = method.getResponseBodyAsStream(); fileName = method.getPath(); Header responseHeader = method.getResponseHeader("Content-Disposition"); if (responseHeader != null) { String value = responseHeader.getValue(); if (value.contains("attachment; filename=\"")) { fileName = value.substring("attachment; filename=\"".length(), value.length() - 1); } } file = createTempFile(fileName); OutputStream targetStream = new FileOutputStream(file); try { IOUtils.copy(sourceStream, targetStream); } catch (Exception e) { IOUtils.closeQuietly(targetStream); FileUtils.deleteQuietly(file); throw e; } finally { IOUtils.closeQuietly(sourceStream); IOUtils.closeQuietly(targetStream); method.releaseConnection(); } } finally { method.releaseConnection(); httpClient.getHttpConnectionManager().closeIdleConnections(0); } Header contentType = method.getResponseHeader("Content-Type"); if ((contentType != null && "application/zip".equals(contentType.getValue())) || fileName.endsWith(".zip")) { file = unzip(file); } if (file != null) { logger.debug("Downloaded " + file.getName()); } return file; } private HostConfiguration getHostConfiguration(URL url) { HostConfiguration hostConfiguration = null; if (url.getHost().endsWith(".i2p")) { hostConfiguration = new HostConfiguration(); hostConfiguration.setProxy(i2pProxyHost, i2pProxyPort); } if (url.getHost().endsWith(".onion")) { hostConfiguration = new HostConfiguration(); hostConfiguration.setProxy(onionProxyHost, onionProxyPort); } return hostConfiguration; } private HttpClient getHttpClient() { HttpConnectionManager httpConnectionManager = new SimpleHttpConnectionManager(false); HttpConnectionManagerParams params = httpConnectionManager.getParams(); params.setDefaultMaxConnectionsPerHost(20); params.setMaxTotalConnections(30); params.setLinger(10); return new HttpClient(httpConnectionManager); } private File unzip(File file) throws Exception { File tempDir = TempFileUtil.createTempDir(); File tempFile = null; ZipInputStream inputStream = new ZipInputStream(new FileInputStream(file)); try { ZipEntry zipEntry; int archiveFileCount = 0; while ((zipEntry = inputStream.getNextEntry()) != null) { if (zipEntry.getName().endsWith(".fbd")) { continue; } tempFile = new File(tempDir, zipEntry.getName()); OutputStream outputStream = new FileOutputStream(tempFile); try { IOUtils.copy(inputStream, outputStream); } finally { IOUtils.closeQuietly(outputStream); } if (++archiveFileCount > 1) { throw new Exception("Multifile archives not supported"); } } } finally { IOUtils.closeQuietly(inputStream); file.delete(); } if (tempFile == null) { FileUtils.deleteDirectory(tempDir); return null; } // move unpacked file to upper level and remove temporary directory File systemTempDir = new File(System.getProperty("java.io.tmpdir")); String tempFileName = tempFile.getName(); boolean isFileMoved = false; int tryCount = 20; File targetFile; do { targetFile = new File(systemTempDir, tempFileName); try { FileUtils.moveFile(tempFile, targetFile); isFileMoved = true; } catch (IOException e) { tempFileName = tempFileName.substring(0, 1) + tempFileName; } } while (!isFileMoved && tryCount-- > 0); FileUtils.deleteDirectory(tempDir); if (!isFileMoved) { throw new IOException("Can't move file " + tempFile.getName() + " to " + systemTempDir.getName()); } return targetFile; } private File createTempFile(String path) throws IOException { String name = FilenameUtils.getName(path); if (name.length() == 0) { return File.createTempFile("tmp", null); } File systemTempDir = new File(System.getProperty("java.io.tmpdir")); return new File(systemTempDir, name); } public void shutdown() { fetchExecutor.shutdownNow(); } public void setFetchPoolSize(int fetchPoolSize) { this.fetchPoolSize = fetchPoolSize; } public void setFetchTimeoutSeconds(long fetchTimeoutSeconds) { this.fetchTimeoutSeconds = fetchTimeoutSeconds; } }