/* * (C) Copyright 2011-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * tdelprat */ package org.nuxeo.wizard.download; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthScope; import org.apache.http.auth.NTCredentials; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.params.ConnRoutePNames; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import org.apache.http.util.EntityUtils; import org.nuxeo.common.Environment; import org.nuxeo.launcher.config.ConfigurationGenerator; /** * @author Tiry (tdelprat@nuxeo.com) */ public class PackageDownloader { protected final static Log log = LogFactory.getLog(PackageDownloader.class); public static final String PACKAGES_XML = "packages.xml"; public static final String PACKAGES_DEFAULT_SELECTION = "packages-default-selection.properties"; public static final String PACKAGES_DEFAULT_SELECTION_PRESETS = "preset"; public static final String PACKAGES_DEFAULT_SELECTION_PACKAGES = "packages"; protected static final int NB_DOWNLOAD_THREADS = 3; protected static final int NB_CHECK_THREADS = 1; protected static final int QUEUESIZE = 20; public static final String BASE_URL_KEY = "nuxeo.wizard.packages.url"; public static final String DEFAULT_BASE_URL = "http://cdn.nuxeo.com/"; // nuxeo-XXX/mp protected CopyOnWriteArrayList<PendingDownload> pendingDownloads = new CopyOnWriteArrayList<>(); protected static PackageDownloader instance; protected DefaultHttpClient httpClient; protected Boolean canReachServer = null; protected DownloadablePackageOptions downloadOptions; protected static final String DIGEST = "MD5"; protected static final int DIGEST_CHUNK = 1024 * 100; boolean downloadStarted = false; protected String lastSelectionDigest; protected final AtomicInteger dwThreadCount = new AtomicInteger(0); protected final AtomicInteger checkThreadCount = new AtomicInteger(0); protected String baseUrl; protected ConfigurationGenerator configurationGenerator = null; protected ConfigurationGenerator getConfig() { if (configurationGenerator == null) { configurationGenerator = new ConfigurationGenerator(); configurationGenerator.init(); } return configurationGenerator; } protected String getBaseUrl() { if (baseUrl == null) { String base = getConfig().getUserConfig().getProperty(BASE_URL_KEY, ""); if ("".equals(base)) { base = DEFAULT_BASE_URL + "nuxeo-" + getConfig().getUserConfig().getProperty(Environment.DISTRIBUTION_VERSION) + "/mp/"; } if (!base.endsWith("/")) { base = base + "/"; } baseUrl = base; } return baseUrl; } protected ThreadPoolExecutor download_tpe = new ThreadPoolExecutor(NB_DOWNLOAD_THREADS, NB_DOWNLOAD_THREADS, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(QUEUESIZE), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); t.setName("DownloaderThread-" + dwThreadCount.incrementAndGet()); return t; } }); protected ThreadPoolExecutor check_tpe = new ThreadPoolExecutor(NB_CHECK_THREADS, NB_CHECK_THREADS, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(QUEUESIZE), r -> { Thread t = new Thread(r); t.setDaemon(true); t.setName("MD5CheckThread-" + checkThreadCount.incrementAndGet()); return t; }); protected PackageDownloader() { SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); registry.register(new Scheme("https", 443, SSLSocketFactory.getSocketFactory())); HttpParams httpParams = new BasicHttpParams(); HttpProtocolParams.setUseExpectContinue(httpParams, false); ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(registry); cm.setMaxTotal(NB_DOWNLOAD_THREADS); cm.setDefaultMaxPerRoute(NB_DOWNLOAD_THREADS); httpClient = new DefaultHttpClient(cm, httpParams); } public synchronized static PackageDownloader instance() { if (instance == null) { instance = new PackageDownloader(); instance.download_tpe.prestartAllCoreThreads(); instance.check_tpe.prestartAllCoreThreads(); } return instance; } public static void reset() { if (instance != null) { instance.shutdown(); instance = null; } } public void setProxy(String proxy, int port, String login, String password, String NTLMHost, String NTLMDomain) { if (proxy != null) { HttpHost proxyHost = new HttpHost(proxy, port); httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxyHost); if (login != null) { if (NTLMHost != null && !NTLMHost.trim().isEmpty()) { NTCredentials ntlmCredentials = new NTCredentials(login, password, NTLMHost, NTLMDomain); httpClient.getCredentialsProvider().setCredentials(new AuthScope(proxy, port), ntlmCredentials); } else { httpClient.getCredentialsProvider().setCredentials(new AuthScope(proxy, port), new UsernamePasswordCredentials(login, password)); } } else { httpClient.getCredentialsProvider().clear(); } } else { httpClient.getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY); httpClient.getCredentialsProvider().clear(); } } protected String getSelectionDigest(List<String> ids) { List<String> lst = new ArrayList<>(ids); Collections.sort(lst); StringBuilder sb = new StringBuilder(); for (String item : lst) { sb.append(item); sb.append(":"); } return sb.toString(); } public void selectOptions(List<String> ids) { String newSelectionDigest = getSelectionDigest(ids); if (lastSelectionDigest != null) { if (lastSelectionDigest.equals(newSelectionDigest)) { return; } } getPackageOptions().select(ids); downloadStarted = false; lastSelectionDigest = newSelectionDigest; } protected File getDownloadDirectory() { File mpDir = getConfig().getDistributionMPDir(); if (!mpDir.exists()) { mpDir.mkdirs(); } return mpDir; } public boolean canReachServer() { if (canReachServer == null) { HttpGet ping = new HttpGet(getBaseUrl() + PACKAGES_XML); try { HttpResponse response = httpClient.execute(ping); if (response.getStatusLine().getStatusCode() == 200) { canReachServer = true; } else { log.info("Unable to ping server -> status code :" + response.getStatusLine().getStatusCode() + " (" + response.getStatusLine().getReasonPhrase() + ")"); canReachServer = false; } } catch (Exception e) { log.info("Unable to ping remote server " + e.getMessage()); log.debug("Unable to ping remote server", e); canReachServer = false; } } return canReachServer; } public DownloadablePackageOptions getPackageOptions() { if (downloadOptions == null) { File packageFile = null; if (canReachServer()) { packageFile = getRemotePackagesDescriptor(); } if (packageFile == null) { packageFile = getLocalPackagesDescriptor(); if (packageFile == null) { log.warn("Unable to find local copy of packages.xml"); } else { log.info("Wizard will use the local copy of packages.xml."); } } if (packageFile != null) { try { downloadOptions = DownloadDescriptorParser.parsePackages(new FileInputStream(packageFile)); // manage init from presets if available Properties defaultSelection = getDefaultPackageSelection(); if (defaultSelection != null) { String presetId = defaultSelection.getProperty(PACKAGES_DEFAULT_SELECTION_PRESETS, null); if (presetId != null && !presetId.isEmpty()) { for (Preset preset : downloadOptions.getPresets()) { if (preset.getId().equals(presetId)) { List<String> pkgIds = Arrays.asList(preset.getPkgs()); downloadOptions.select(pkgIds); break; } } } else { String pkgIdsList = defaultSelection.getProperty(PACKAGES_DEFAULT_SELECTION_PACKAGES, null); if (pkgIdsList != null && !pkgIdsList.isEmpty()) { String[] ids = pkgIdsList.split(","); List<String> pkgIds = Arrays.asList(ids); downloadOptions.select(pkgIds); } } } } catch (FileNotFoundException e) { log.error("Unable to read packages.xml", e); } } } return downloadOptions; } protected File getRemotePackagesDescriptor() { File desc; HttpGet ping = new HttpGet(getBaseUrl() + PACKAGES_XML); try { HttpResponse response = httpClient.execute(ping); if (response.getStatusLine().getStatusCode() == 200) { desc = new File(getDownloadDirectory(), PACKAGES_XML); FileUtils.copyInputStreamToFile(response.getEntity().getContent(), desc); } else { log.warn("Unable to download remote packages.xml, status code :" + response.getStatusLine().getStatusCode() + " (" + response.getStatusLine().getReasonPhrase() + ")"); return null; } } catch (Exception e) { log.warn("Unable to reach remote packages.xml", e); return null; } return desc; } protected Properties getDefaultPackageSelection() { File desc = new File(getDownloadDirectory(), PACKAGES_DEFAULT_SELECTION); if (desc.exists()) { try { Properties props = new Properties(); props.load(new FileReader(desc)); return props; } catch (IOException e) { log.warn("Unable to load presets", e); } } return null; } protected void saveSelectedPackages(List<DownloadPackage> pkgs) { File desc = new File(getDownloadDirectory(), PACKAGES_DEFAULT_SELECTION); String defaultSelPackages = pkgs.stream().map(DownloadPackage::getId).collect(Collectors.joining(",")); Properties props = new Properties(); props.put(PACKAGES_DEFAULT_SELECTION_PACKAGES, defaultSelPackages); try { props.store(new FileWriter(desc), "Saved from Nuxeo SetupWizard"); } catch (IOException e) { log.error("Unable to save package selection", e); } } protected File getLocalPackagesDescriptor() { File desc = new File(getDownloadDirectory(), PACKAGES_XML); if (desc.exists()) { return desc; } return null; } public List<DownloadPackage> getSelectedPackages() { List<DownloadPackage> pkgs = getPackageOptions().getPkg4Install(); File[] listFiles = getDownloadDirectory().listFiles(); for (DownloadPackage pkg : pkgs) { for (File file : listFiles) { if (file.getName().equals(pkg.getMd5())) { // recheck md5 ??? pkg.setLocalFile(file); } } needToDownload(pkg); } return pkgs; } public void scheduleDownloadedPackagesForInstallation(String installationFilePath) throws IOException { List<String> fileEntries = new ArrayList<>(); fileEntries.add("init"); List<DownloadPackage> pkgs = downloadOptions.getPkg4Install(); List<String> pkgInstallIds = new ArrayList<>(); for (DownloadPackage pkg : pkgs) { if (pkg.isVirtual()) { log.debug("No install for virtual package: " + pkg.getId()); } else if (pkg.isAlreadyInLocal() || StringUtils.isBlank(pkg.getFilename())) { // Blank filename means later downloaded fileEntries.add("install " + pkg.getId()); pkgInstallIds.add(pkg.getId()); } else { for (PendingDownload download : pendingDownloads) { if (download.getPkg().equals(pkg)) { if (download.getStatus() == PendingDownloadStatus.VERIFIED) { File file = download.getDowloadingFile(); fileEntries.add("add file:" + file.getAbsolutePath()); fileEntries.add("install " + pkg.getId()); pkgInstallIds.add(pkg.getId()); } else { log.error("One selected package has not been downloaded : " + pkg.getId()); } } } } } File installLog = new File(installationFilePath); if (fileEntries.size() > 0) { if (!installLog.exists()) { File parent = installLog.getParentFile(); if (!parent.exists()) { parent.mkdirs(); } installLog.createNewFile(); } FileUtils.writeLines(installLog, fileEntries); } else { // Should not happen as the file always has "init" if (installLog.exists()) { installLog.delete(); } } // Save presets saveSelectedPackages(pkgs); } public List<PendingDownload> getPendingDownloads() { return pendingDownloads; } public void reStartDownload(String id) { for (PendingDownload pending : pendingDownloads) { if (pending.getPkg().getId().equals(id)) { if (Arrays.asList(PendingDownloadStatus.CORRUPTED, PendingDownloadStatus.ABORTED).contains( pending.getStatus())) { pendingDownloads.remove(pending); startDownloadPackage(pending.getPkg()); } break; } } } public void startDownload() { startDownload(downloadOptions.getPkg4Install()); } public void startDownload(List<DownloadPackage> pkgs) { downloadStarted = true; for (DownloadPackage pkg : pkgs) { if (needToDownload(pkg)) { startDownloadPackage(pkg); } } } protected boolean needToDownload(DownloadPackage pkg) { return !pkg.isVirtual() && !pkg.isLaterDownload() && !pkg.isAlreadyInLocal(); } protected void startDownloadPackage(final DownloadPackage pkg) { final PendingDownload download = new PendingDownload(pkg); if (pendingDownloads.addIfAbsent(download)) { Runnable downloadRunner = () -> { log.info("Starting download on Thread " + Thread.currentThread().getName()); download.setStatus(PendingDownloadStatus.INPROGRESS); String url = pkg.getDownloadUrl(); if (!url.startsWith("http")) { url = getBaseUrl() + url; } File filePkg; HttpGet dw = new HttpGet(url); try { HttpResponse response = httpClient.execute(dw); if (response.getStatusLine().getStatusCode() == 200) { filePkg = new File(getDownloadDirectory(), pkg.filename); Header clh = response.getFirstHeader("Content-Length"); if (clh != null) { long filesize = Long.parseLong(clh.getValue()); download.setFile(filesize, filePkg); } FileUtils.copyInputStreamToFile(response.getEntity().getContent(), filePkg); download.setStatus(PendingDownloadStatus.COMPLETED); } else if (response.getStatusLine().getStatusCode() == 404) { log.error("Package " + pkg.filename + " not found :" + url); download.setStatus(PendingDownloadStatus.MISSING); EntityUtils.consume(response.getEntity()); dw.abort(); return; } else { log.error("Received StatusCode " + response.getStatusLine().getStatusCode()); download.setStatus(PendingDownloadStatus.ABORTED); EntityUtils.consume(response.getEntity()); dw.abort(); return; } } catch (Exception e) { download.setStatus(PendingDownloadStatus.ABORTED); log.error("Error during download", e); return; } checkPackage(download); }; download_tpe.execute(downloadRunner); } } protected void checkPackage(final PendingDownload download) { final File filePkg = download.getDowloadingFile(); Runnable checkRunner = () -> { download.setStatus(PendingDownloadStatus.VERIFICATION); String expectedDigest = download.getPkg().getMd5(); String digest = getDigest(filePkg); if (digest == null || (expectedDigest != null && !expectedDigest.equals(digest))) { download.setStatus(PendingDownloadStatus.CORRUPTED); log.error("Digest check failed: expected=" + expectedDigest + " computed=" + digest); return; } File newFile = new File(getDownloadDirectory(), digest); filePkg.renameTo(newFile); download.setStatus(PendingDownloadStatus.VERIFIED); download.setFile(newFile.length(), newFile); }; check_tpe.execute(checkRunner); } protected String getDigest(File file) { try { MessageDigest md = MessageDigest.getInstance(DIGEST); byte[] buffer = new byte[DIGEST_CHUNK]; InputStream stream = new FileInputStream(file); int bytesRead; while ((bytesRead = stream.read(buffer)) >= 0) { md.update(buffer, 0, bytesRead); } stream.close(); byte[] b = md.digest(); return md5ToHex(b); } catch (Exception e) { log.error("Error while computing Digest ", e); return null; } } protected static String md5ToHex(byte[] hash) { StringBuilder hexString = new StringBuilder(); for (byte b : hash) { String hex = Integer.toHexString(0xFF & b); if (hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } return hexString.toString(); } public boolean isDownloadStarted() { return downloadStarted; } public boolean isDownloadCompleted() { if (!isDownloadStarted()) { return false; } for (PendingDownload download : pendingDownloads) { if (download.getStatus().getValue() < PendingDownloadStatus.VERIFIED.getValue()) { return false; } } return true; } public boolean isDownloadInProgress() { if (!isDownloadStarted()) { return false; } if (isDownloadCompleted()) { return false; } int nbInProgress = 0; for (PendingDownload download : pendingDownloads) { if (download.getStatus().getValue() < PendingDownloadStatus.VERIFIED.getValue() && download.getStatus().getValue() >= PendingDownloadStatus.PENDING.getValue()) { nbInProgress++; } } return nbInProgress > 0; } public void shutdown() { if (httpClient != null) { httpClient.getConnectionManager().shutdown(); } download_tpe.shutdownNow(); check_tpe.shutdownNow(); } }