af8b7bf237
source and destination files for differences since Windows (and probably other platforms) doesn't set the file modification time to the precise time requested. Don't notify the observer when we're finished downloading all files until we've finished all of our work with the file. Check the return value from the call to setLastModified(). git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1598 542714f4-19e9-0310-aa3c-eee0fc999fb1
360 lines
12 KiB
Java
360 lines
12 KiB
Java
//
|
|
// $Id: DownloadManager.java,v 1.3 2002/07/22 20:45:22 shaper Exp $
|
|
|
|
package com.threerings.resource;
|
|
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URL;
|
|
import java.net.URLConnection;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import com.samskivert.util.Queue;
|
|
import com.samskivert.util.StringUtil;
|
|
|
|
/**
|
|
* Manages the asynchronous downloading of files that are usually located
|
|
* on an HTTP server but that may also be located on the local filesystem.
|
|
*/
|
|
public class DownloadManager
|
|
{
|
|
/**
|
|
* Provides facilities for notifying an observer of file download
|
|
* progress.
|
|
*/
|
|
public interface DownloadObserver
|
|
{
|
|
/**
|
|
* Called when the download manager is about to check all
|
|
* downloads to see whether they are in need of an update.
|
|
*/
|
|
public void resolvingDownloads ();
|
|
|
|
/**
|
|
* Called to inform the observer of ongoing progress toward
|
|
* completion of the overall downloading task. The caller is
|
|
* guaranteed to get at least one call reporting 100% completion.
|
|
*
|
|
* @param percent the percent completion, in terms of total file
|
|
* size, of the download request.
|
|
*/
|
|
public void downloadProgress (int percent);
|
|
|
|
/**
|
|
* Called if a failure occurs while checking for an update or
|
|
* downloading a file.
|
|
*
|
|
* @param desc the file that was being downloaded when the error
|
|
* occurred, or <code>null</code> if the failure occurred while
|
|
* resolving downloads.
|
|
* @param e the exception detailing the failure.
|
|
*/
|
|
public void downloadFailed (DownloadDescriptor desc, Exception e);
|
|
}
|
|
|
|
/**
|
|
* Describes a single file to be downloaded.
|
|
*/
|
|
public static class DownloadDescriptor
|
|
{
|
|
/** The URL from which the file is to be downloaded. */
|
|
public URL sourceURL;
|
|
|
|
/** The destination file to which the file is to be written. */
|
|
public File destFile;
|
|
|
|
/** The last-modified timestamp of the source file. */
|
|
public long lastModified;
|
|
|
|
/** The size in bytes of the source files. */
|
|
public long fileSize;
|
|
|
|
/** The last-modified timestamp of the destination file. */
|
|
public long destLastModified;
|
|
|
|
/** The size in bytes of the destination file. */
|
|
public long destFileSize;
|
|
|
|
/**
|
|
* Constructs a download descriptor to retrieve the given file
|
|
* from the given URL.
|
|
*/
|
|
public DownloadDescriptor (URL url, File file)
|
|
{
|
|
this.sourceURL = url;
|
|
this.destFile = file;
|
|
}
|
|
|
|
/** Returns a string representation of this instance. */
|
|
public String toString ()
|
|
{
|
|
return StringUtil.fieldsToString(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Downloads the supplied list of descriptors, notifying the given
|
|
* download observer of download progress and status.
|
|
*
|
|
* @param descriptors the list of files to be downloaded.
|
|
* @param fragile if true, reports failure and ceases any further
|
|
* downloading if an error occurs; else will continue attempting to
|
|
* download the remainder of the descriptors.
|
|
* @param downloadObs the observer to notify of progress, success and
|
|
* failure.
|
|
*/
|
|
public void download (
|
|
List descriptors, boolean fragile, DownloadObserver downloadObs)
|
|
{
|
|
// add the download request to the download queue
|
|
DownloadRecord dlrec = new DownloadRecord();
|
|
dlrec.descriptors = descriptors;
|
|
dlrec.obs = downloadObs;
|
|
dlrec.fragile = fragile;
|
|
_dlqueue.append(dlrec);
|
|
|
|
synchronized (this) {
|
|
// if we've not yet got our downloading thread...
|
|
if (_dlthread == null) {
|
|
// create the thread
|
|
_dlthread = new Thread() {
|
|
public void run () {
|
|
processDownloads();
|
|
}
|
|
};
|
|
_dlthread.setDaemon(true);
|
|
// and start it going
|
|
_dlthread.start();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by the download thread to process all download requests in
|
|
* the download queue.
|
|
*/
|
|
protected void processDownloads ()
|
|
{
|
|
// create the data buffer to be used when reading files
|
|
_buffer = new byte[BUFFER_SIZE];
|
|
|
|
DownloadRecord dlrec;
|
|
while (true) {
|
|
synchronized (this) {
|
|
// kill the download thread if we have no remaining
|
|
// download requests
|
|
if (_dlqueue.size() == 0) {
|
|
_dlthread = null;
|
|
// free up the data buffer
|
|
_buffer = null;
|
|
return;
|
|
}
|
|
|
|
// pop download off the queue
|
|
dlrec = (DownloadRecord)_dlqueue.getNonBlocking();
|
|
}
|
|
|
|
// handle the download request
|
|
processDownloadRequest(dlrec.descriptors, dlrec.obs, dlrec.fragile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes a single download request.
|
|
*/
|
|
protected void processDownloadRequest (
|
|
List descriptors, DownloadObserver obs, boolean fragile)
|
|
{
|
|
// let the observer know that we're about to resolve all files to
|
|
// be downloaded
|
|
obs.resolvingDownloads();
|
|
|
|
// check the size and last-modified information for each file to
|
|
// ascertain whether our local copy needs to be refreshed
|
|
ArrayList fetch = new ArrayList();
|
|
long totalSize = 0;
|
|
int size = descriptors.size();
|
|
for (int ii = 0; ii < size; ii++) {
|
|
DownloadDescriptor desc = (DownloadDescriptor)descriptors.get(ii);
|
|
|
|
try {
|
|
// get the source file information
|
|
desc.fileSize = 0;
|
|
desc.lastModified = 0;
|
|
String protocol = desc.sourceURL.getProtocol();
|
|
if (protocol.equals("file")) {
|
|
// read the file information directly from the file system
|
|
File tfile = new File(desc.sourceURL.getPath());
|
|
desc.fileSize = tfile.length();
|
|
desc.lastModified = tfile.lastModified();
|
|
|
|
} else if (protocol.equals("http")) {
|
|
// read the file information via an HTTP HEAD request
|
|
HttpURLConnection ucon = (HttpURLConnection)
|
|
desc.sourceURL.openConnection();
|
|
ucon.setRequestMethod("HEAD");
|
|
ucon.connect();
|
|
desc.fileSize = ucon.getContentLength();
|
|
desc.lastModified = ucon.getLastModified();
|
|
|
|
} else {
|
|
throw new IOException(
|
|
"Unknown source file protocol " +
|
|
"[protocol=" + protocol + ", desc=" + desc + "].");
|
|
}
|
|
|
|
// determine whether we actually need to fetch the file by
|
|
// checking to see whether we have a local copy at all,
|
|
// and if so, whether its file size or last-modified time
|
|
// differs from the source file
|
|
desc.destFileSize = desc.destFile.length();
|
|
desc.destLastModified = desc.destFile.lastModified();
|
|
if ((!desc.destFile.exists()) ||
|
|
(desc.destFileSize != desc.fileSize) ||
|
|
(desc.destLastModified < (desc.lastModified - DELTA)) ||
|
|
(desc.destLastModified > (desc.lastModified + DELTA))) {
|
|
// increment the total file size to be fetched
|
|
totalSize += desc.fileSize;
|
|
// add the file to the list of files to be fetched
|
|
fetch.add(desc);
|
|
Log.debug("File deemed stale " +
|
|
"[url=" + desc.sourceURL + "].");
|
|
|
|
} else {
|
|
Log.debug("File deemed up-to-date " +
|
|
"[url=" + desc.sourceURL + "].");
|
|
}
|
|
|
|
} catch (IOException ioe) {
|
|
obs.downloadFailed(null, ioe);
|
|
if (fragile) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// download all stale files
|
|
size = fetch.size();
|
|
long currentSize = 0;
|
|
boolean complete = false;
|
|
for (int ii = 0; ii < size; ii++) {
|
|
DownloadDescriptor desc = (DownloadDescriptor)fetch.get(ii);
|
|
try {
|
|
complete = processDownload(desc, obs, currentSize, totalSize);
|
|
currentSize += desc.fileSize;
|
|
|
|
} catch (IOException ioe) {
|
|
obs.downloadFailed(desc, ioe);
|
|
if (fragile) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// make sure to always let the observer know that we've wrapped up
|
|
// by reporting 100% completion
|
|
if (!complete) {
|
|
obs.downloadProgress(100);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes a single download descriptor. Returns whether the
|
|
* download observer was notified of a 100% complete progress update.
|
|
*/
|
|
protected boolean processDownload (
|
|
DownloadDescriptor desc, DownloadObserver obs, long currentSize,
|
|
long totalSize)
|
|
throws IOException
|
|
{
|
|
// download the resource bundle from the specified URL
|
|
URLConnection ucon = desc.sourceURL.openConnection();
|
|
|
|
// prepare to read the data from the URL into the cache file
|
|
Log.info("Downloading file [url=" + desc.sourceURL + "].");
|
|
InputStream in = ucon.getInputStream();
|
|
FileOutputStream out = new FileOutputStream(desc.destFile);
|
|
int read;
|
|
boolean complete = false;
|
|
|
|
// read in the file data
|
|
while ((read = in.read(_buffer)) != -1) {
|
|
// write it out to our local copy
|
|
out.write(_buffer, 0, read);
|
|
|
|
// report our progress to the download observer as a
|
|
// percentage of the total file data to be transferred
|
|
currentSize += read;
|
|
int pctdone = (int)((currentSize / (float)totalSize) * 100f);
|
|
complete = (pctdone >= 100);
|
|
// if we've finished downloading everything, hold off on
|
|
// notifying the observer until we're done working with the
|
|
// file to ensure that any action the observer may take with
|
|
// respect to the downloaded files can be safely undertaken
|
|
if (!complete) {
|
|
obs.downloadProgress(pctdone);
|
|
}
|
|
}
|
|
|
|
// close the streams
|
|
in.close();
|
|
out.close();
|
|
|
|
// if we have a last modified time, we want to adjust our cache
|
|
// file accordingly
|
|
if (desc.lastModified != 0) {
|
|
if (!desc.destFile.setLastModified(desc.lastModified)) {
|
|
Log.warning("Failed to set last-modified date " +
|
|
"[file=" + desc.destFile + "].");
|
|
}
|
|
}
|
|
|
|
if (complete) {
|
|
// let the observer know we're finished now that we've
|
|
// finished all of our work with the file
|
|
obs.downloadProgress(100);
|
|
}
|
|
|
|
return complete;
|
|
}
|
|
|
|
/**
|
|
* A record describing a single download request.
|
|
*/
|
|
protected static class DownloadRecord
|
|
{
|
|
/** The list of download descriptors to be downloaded. */
|
|
public List descriptors;
|
|
|
|
/** The download observer to notify of download progress. */
|
|
public DownloadObserver obs;
|
|
|
|
/** Whether to abort downloading if an error occurs. */
|
|
public boolean fragile;
|
|
}
|
|
|
|
/** The downloading thread. */
|
|
protected Thread _dlthread;
|
|
|
|
/** The queue of download requests. */
|
|
protected Queue _dlqueue = new Queue();
|
|
|
|
/** The data buffer used when reading file data. */
|
|
protected byte[] _buffer;
|
|
|
|
/** The data buffer size for reading file data. */
|
|
protected static final int BUFFER_SIZE = 2048;
|
|
|
|
/** The last-modified difference in milliseconds allowed between the
|
|
* source and destination file without considering the destination
|
|
* file to be out of date. */
|
|
protected static final long DELTA = 5000L;
|
|
}
|