// // Getdown - application installer, patcher and launcher // Copyright (C) 2004-2018 Getdown authors // https://github.com/threerings/getdown/blob/master/LICENSE package com.threerings.getdown.net; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import com.threerings.getdown.data.Resource; import static com.threerings.getdown.Log.log; /** * Handles the download of a collection of files, first issuing HTTP head requests to obtain size * information and then downloading the files individually, reporting progress back via protected * callback methods. Note: these methods are all called arbitrary download threads, so * implementors must take care to only execute thread-safe code or simply pass a message to the AWT * thread, for example. */ public abstract class Downloader { /** * Start the downloading process. * @param resources the resources to download. * @param maxConcurrent the maximum number of concurrent downloads allowed. * @return true if the download completed, false if it was aborted (via {@link #abort}). */ public boolean download (Collection resources, int maxConcurrent) { // first compute the total size of our download resolvingDownloads(); for (Resource rsrc : resources) { try { _sizes.put(rsrc, Math.max(checkSize(rsrc), 0L)); } catch (IOException ioe) { downloadFailed(rsrc, ioe); } } long totalSize = sum(_sizes.values()); log.info("Downloading " + resources.size() + " resources", "totalBytes", totalSize, "maxConcurrent", maxConcurrent); // make a note of the time at which we started the download _start = System.currentTimeMillis(); // start the downloads ExecutorService exec = Executors.newFixedThreadPool(maxConcurrent); for (final Resource rsrc : resources) { // make sure the resource's target directory exists File parent = new File(rsrc.getLocal().getParent()); if (!parent.exists() && !parent.mkdirs()) { log.warning("Failed to create target directory for resource '" + rsrc + "'."); } exec.execute(new Runnable() { @Override public void run () { try { if (_state != State.ABORTED) { download(rsrc); } } catch (IOException ioe) { _state = State.FAILED; downloadFailed(rsrc, ioe); } } }); } exec.shutdown(); // wait for the downloads to complete try { exec.awaitTermination(10, TimeUnit.DAYS); // report download completion if we did not already do so via our final resource if (_state == State.DOWNLOADING) { downloadProgress(100, 0); } } catch (InterruptedException ie) { exec.shutdownNow(); downloadFailed(null, ie); } return _state != State.ABORTED; } /** * Aborts the in-progress download. */ public void abort () { _state = State.ABORTED; } /** * Called before the downloader begins the series of HTTP head requests to determine the * size of the files it needs to download. */ protected void resolvingDownloads () {} /** * Reports ongoing progress toward completion of the overall downloading task. One call is * guaranteed to be made reporting 100% completion if the download is not aborted and no * resources fail. * * @param percent the percent completion of the complete download process (based on total bytes * downloaded versus total byte size of all resources). * @param remaining the estimated download time remaining in seconds, or {@code -1} if the time * can not yet be determined. */ protected void downloadProgress (int percent, long remaining) {} /** * Called if a failure occurs while downloading a resource. No progress will be reported after * a download fails, but additional download failures may be reported. * * @param rsrc the resource that failed to download, or null if the download failed due to * thread interruption. * @param cause the exception detailing the failure. */ protected void downloadFailed (Resource rsrc, Exception cause) {} /** * Performs the protocol-specific portion of checking download size. */ protected abstract long checkSize (Resource rsrc) throws IOException; /** * Periodically called by the protocol-specific downloaders to update their progress. This * should be called at least once for each resource to be downloaded, with the total downloaded * size for that resource. It can also be called periodically along the way for each resource * to communicate incremental progress. * * @param rsrc the resource currently being downloaded. * @param currentSize the number of bytes currently downloaded for said resource. * @param actualSize the size reported for this resource now that we're actually downloading * it. Some web servers lie about Content-length when doing a HEAD request, so by reporting * updated sizes here we can recover from receiving bogus information in the earlier * {@link #checkSize} phase. */ protected synchronized void reportProgress (Resource rsrc, long currentSize, long actualSize) { // update the actual size for this resource (but don't let it shrink) _sizes.put(rsrc, actualSize = Math.max(actualSize, _sizes.get(rsrc))); // update the current downloaded size for said resource; don't allow the downloaded bytes // to exceed the original claimed size of the resource, otherwise our progress will get // booched and we'll end up back on the Daily WTF: http://tinyurl.com/29wt4oq _downloaded.put(rsrc, Math.min(actualSize, currentSize)); // notify the observer if it's been sufficiently long since our last notification long now = System.currentTimeMillis(); if ((now - _lastUpdate) >= UPDATE_DELAY) { _lastUpdate = now; // total up our current and total bytes long downloaded = sum(_downloaded.values()); long totalSize = sum(_sizes.values()); // compute our bytes per second long secs = (now - _start) / 1000L; long bps = (secs == 0) ? 0 : (downloaded / secs); // compute our percentage completion int pctdone = (totalSize == 0) ? 0 : (int)((downloaded * 100f) / totalSize); // estimate our time remaining long remaining = (bps <= 0 || totalSize == 0) ? -1 : (totalSize - downloaded) / bps; // if we're complete or failed, when we don't want to report again if (_state == State.DOWNLOADING) { if (pctdone == 100) _state = State.COMPLETE; downloadProgress(pctdone, remaining); } } } /** * Sums the supplied values. */ protected static long sum (Iterable values) { long acc = 0L; for (Long value : values) { acc += value; } return acc; } protected enum State { DOWNLOADING, COMPLETE, FAILED, ABORTED } /** * Accomplishes the copying of the resource from remote location to local location using * protocol-specific code. This method should periodically check whether {@code _state} is set * to aborted and abort any in-progress download if so. */ protected abstract void download (Resource rsrc) throws IOException; /** The reported sizes of our resources. */ protected Map _sizes = new HashMap<>(); /** The bytes downloaded for each resource. */ protected Map _downloaded = new HashMap<>(); /** The time at which the file transfer began. */ protected long _start; /** The current transfer rate in bytes per second. */ protected long _bytesPerSecond; /** The time at which the last progress update was posted to the progress observer. */ protected long _lastUpdate; /** A wee state machine to ensure we call our callbacks sanely. */ protected volatile State _state = State.DOWNLOADING; /** The delay in milliseconds between notifying progress observers of file download * progress. */ protected static final long UPDATE_DELAY = 500L; }