diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..160c7f7
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/LIBS b/lib/LIBS
new file mode 100644
index 0000000..2d57f1c
--- /dev/null
+++ b/lib/LIBS
@@ -0,0 +1,2 @@
+samskivert.jar
+ant.jar
diff --git a/lib/manifest.mf b/lib/manifest.mf
new file mode 100644
index 0000000..ed06cbd
--- /dev/null
+++ b/lib/manifest.mf
@@ -0,0 +1 @@
+Main-Class: com.threerings.getdown.launcher.Getdown
diff --git a/src/java/com/threerings/getdown/Log.java b/src/java/com/threerings/getdown/Log.java
new file mode 100644
index 0000000..132fd4f
--- /dev/null
+++ b/src/java/com/threerings/getdown/Log.java
@@ -0,0 +1,38 @@
+//
+// $Id: Log.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+
+package com.threerings.getdown;
+
+/**
+ * A placeholder class that contains a reference to the log object used by
+ * the Getdown code.
+ */
+public class Log
+{
+ public static com.samskivert.util.Log log =
+ new com.samskivert.util.Log("getdown");
+
+ /** Convenience function. */
+ public static void debug (String message)
+ {
+ log.debug(message);
+ }
+
+ /** Convenience function. */
+ public static void info (String message)
+ {
+ log.info(message);
+ }
+
+ /** Convenience function. */
+ public static void warning (String message)
+ {
+ log.warning(message);
+ }
+
+ /** Convenience function. */
+ public static void logStackTrace (Throwable t)
+ {
+ log.logStackTrace(com.samskivert.util.Log.WARNING, t);
+ }
+}
diff --git a/src/java/com/threerings/getdown/data/Application.java b/src/java/com/threerings/getdown/data/Application.java
new file mode 100644
index 0000000..296bacf
--- /dev/null
+++ b/src/java/com/threerings/getdown/data/Application.java
@@ -0,0 +1,185 @@
+//
+// $Id: Application.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+
+package com.threerings.getdown.data;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import com.samskivert.io.NestableIOException;
+import com.samskivert.text.MessageUtil;
+import com.samskivert.util.StringUtil;
+
+import com.threerings.getdown.Log;
+import com.threerings.getdown.util.ConfigUtil;
+
+/**
+ * Parses and provide access to the information contained in the
+ * getdown.txt configuration file.
+ */
+public class Application
+{
+ /** The name of our configuration file. */
+ public static final String CONFIG_FILE = "getdown.txt";
+
+ /**
+ * Creates an application instance which records the location of the
+ * getdown.txt configuration file from the supplied
+ * application directory.
+ */
+ public Application (File appdir)
+ {
+ _appdir = appdir;
+ _config = new File(appdir, CONFIG_FILE);
+ }
+
+ /**
+ * Returns a resource that refers to the application configuration
+ * file itself.
+ */
+ public Resource getConfigResource ()
+ {
+ try {
+ return new Resource(_appbase, _appdir, CONFIG_FILE);
+ } catch (Exception e) {
+ throw new RuntimeException("Booched appbase '" + _appbase + "'!?");
+ }
+ }
+
+ /**
+ * Returns a list of the code {@link Resource} objects used by this
+ * application.
+ */
+ public List getCodeResources ()
+ {
+ return _codes;
+ }
+
+ /**
+ * Returns a list of the non-code {@link Resource} objects used by
+ * this application.
+ */
+ public List getResources ()
+ {
+ return _resources;
+ }
+
+ /**
+ * Instructs the application to parse its getdown.txt
+ * configuration and prepare itself for operation. The application
+ * base URL will be parsed first so that if there are errors
+ * discovered later, the caller can use the application base to
+ * download a new config.txt file and try again.
+ *
+ * @exception IOException thrown if there is an error reading the file
+ * or an error encountered during its parsing.
+ */
+ public void init ()
+ throws IOException
+ {
+ // parse our configuration file
+ HashMap cdata = ConfigUtil.parseConfig(_config);
+
+ // first determine our application base, this way if anything goes
+ // wrong later in the process, our caller can use the appbase to
+ // download a new configuration file
+ String appbase = (String)cdata.get("appbase");
+ if (appbase == null) {
+ throw new IOException("m.missing_appbase");
+ }
+ try {
+ // make sure there's a trailing slash
+ if (!appbase.endsWith("/")) {
+ appbase = appbase + "/";
+ }
+ _appbase = new URL(appbase);
+ } catch (Exception e) {
+ String err = MessageUtil.tcompose("m.invalid_appbase", appbase);
+ throw new NestableIOException(err, e);
+ }
+
+ // extract our version information
+ String vstr = (String)cdata.get("version");
+ if (vstr != null) {
+ try {
+ _version = Integer.parseInt(vstr);
+ } catch (Exception e) {
+ String err = MessageUtil.tcompose("m.invalid_version", vstr);
+ throw new NestableIOException(err, e);
+ }
+ }
+
+ // determine our application class name
+ _class = (String)cdata.get("class");
+ if (_class == null) {
+ throw new IOException("m.missing_class");
+ }
+
+ // parse our code resources
+ String[] codes = ConfigUtil.getMultiValue(cdata, "code");
+ if (codes == null) {
+ throw new IOException("m.missing_code");
+ }
+ for (int ii = 0; ii < codes.length; ii++) {
+ try {
+ _codes.add(new Resource(_appbase, _appdir, codes[ii]));
+ } catch (Exception e) {
+ Log.warning("Invalid code resource '" + codes[ii] + "'.");
+ }
+ }
+
+ // parse our non-code resources
+ String[] rsrcs = ConfigUtil.getMultiValue(cdata, "resource");
+ if (rsrcs != null) {
+ for (int ii = 0; ii < rsrcs.length; ii++) {
+ try {
+ _resources.add(new Resource(_appbase, _appdir, rsrcs[ii]));
+ } catch (Exception e) {
+ Log.warning("Invalid resource '" + rsrcs[ii] + "'.");
+ }
+ }
+ }
+
+ // transfer our JVM arguments
+ String[] jvmargs = ConfigUtil.getMultiValue(cdata, "jvmarg");
+ if (jvmargs != null) {
+ for (int ii = 0; ii < jvmargs.length; ii++) {
+ _jvmargs.add(jvmargs[ii]);
+ }
+ }
+
+ // transfer our application arguments
+ String[] appargs = ConfigUtil.getMultiValue(cdata, "apparg");
+ if (appargs != null) {
+ for (int ii = 0; ii < appargs.length; ii++) {
+ _appargs.add(appargs[ii]);
+ }
+ }
+
+// Log.info("Parsed application " + _appbase);
+// Log.info("Version: " + _version);
+// Log.info("Class: " + _class);
+// Log.info("Code: " + StringUtil.toString(_codes.iterator()));
+// Log.info("Resources: " + StringUtil.toString(_resources.iterator()));
+// Log.info("JVM Args: " + StringUtil.toString(_jvmargs.iterator()));
+// Log.info("App Args: " + StringUtil.toString(_appargs.iterator()));
+ }
+
+ protected File _appdir;
+ protected File _config;
+
+ protected int _version = -1;
+ protected URL _appbase;
+ protected String _class;
+
+ protected ArrayList _codes = new ArrayList();
+ protected ArrayList _resources = new ArrayList();
+
+ protected ArrayList _jvmargs = new ArrayList();
+ protected ArrayList _appargs = new ArrayList();
+}
diff --git a/src/java/com/threerings/getdown/data/Digest.java b/src/java/com/threerings/getdown/data/Digest.java
new file mode 100644
index 0000000..8322728
--- /dev/null
+++ b/src/java/com/threerings/getdown/data/Digest.java
@@ -0,0 +1,121 @@
+//
+// $Id: Digest.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+
+package com.threerings.getdown.data;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import com.samskivert.text.MessageUtil;
+import com.samskivert.util.StringUtil;
+
+import com.threerings.getdown.util.ConfigUtil;
+
+/**
+ * Manages the digest.txt file and the computing and
+ * processing of MD5 digests for an application.
+ */
+public class Digest
+{
+ /** The name of our MD5 digest file. */
+ public static final String DIGEST_FILE = "digest.txt";
+
+ /**
+ * Creates a digest instance which will parse and validate the
+ * digest.txt in the supplied application directory.
+ */
+ public Digest (File appdir)
+ throws IOException
+ {
+ // parse and validate our digest file contents
+ String metaDigest = "";
+ StringBuffer data = new StringBuffer();
+ List pairs = ConfigUtil.parsePairs(new File(appdir, DIGEST_FILE));
+ for (Iterator iter = pairs.iterator(); iter.hasNext(); ) {
+ String[] pair = (String[])iter.next();
+ if (pair[0].equals(DIGEST_FILE)) {
+ metaDigest = pair[1];
+ break;
+ }
+ _digests.put(pair[0], pair[1]);
+ note(data, pair[0], pair[1]);
+ }
+
+ // we've reached the end, validate our contents
+ MessageDigest md = getMessageDigest();
+ byte[] contents = data.toString().getBytes("UTF-8");
+ String md5 = StringUtil.hexlate(md.digest(contents));
+ if (!md5.equals(metaDigest)) {
+ String err = MessageUtil.tcompose(
+ "m.invalid_digest_file", metaDigest, md5);
+ throw new IOException(err);
+ }
+ }
+
+ /**
+ * Returns the stored digest value for the file with the supplied
+ * path, or null if no digest exists for a file with that path.
+ */
+ public String getDigest (String path)
+ {
+ return (String)_digests.get(path);
+ }
+
+ /**
+ * Creates a digest file at the specified location using the supplied
+ * list of resources.
+ */
+ public static void createDigest (List resources, File output)
+ throws IOException
+ {
+ MessageDigest md = getMessageDigest();
+ StringBuffer data = new StringBuffer();
+ PrintWriter pout = new PrintWriter(
+ new OutputStreamWriter(new FileOutputStream(output), "UTF-8"));
+
+ // compute and append the MD5 digest of each resource in the list
+ for (Iterator iter = resources.iterator(); iter.hasNext(); ) {
+ Resource rsrc = (Resource)iter.next();
+ String path = rsrc.getPath();
+ String digest = rsrc.computeDigest(md);
+ note(data, path, digest);
+ pout.println(path + " = " + digest);
+ }
+
+ // finally compute and append the digest for the file contents
+ md.reset();
+ byte[] contents = data.toString().getBytes("UTF-8");
+ pout.println(DIGEST_FILE + " = " +
+ StringUtil.hexlate(md.digest(contents)));
+
+ pout.close();
+ }
+
+ /** Used by {@link #createDigest} and {@link Digest}. */
+ protected static void note (StringBuffer data, String path, String digest)
+ {
+ data.append(path).append(" = ").append(digest).append("\n");
+ }
+
+ /** Used by {@link #createDigest} and {@link Digest}. */
+ protected static MessageDigest getMessageDigest ()
+ {
+ try {
+ return MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException nsae) {
+ throw new RuntimeException("JVM does not support MD5. Gurp!");
+ }
+ }
+
+ protected HashMap _digests = new HashMap();
+}
diff --git a/src/java/com/threerings/getdown/data/Resource.java b/src/java/com/threerings/getdown/data/Resource.java
new file mode 100644
index 0000000..5700cc9
--- /dev/null
+++ b/src/java/com/threerings/getdown/data/Resource.java
@@ -0,0 +1,86 @@
+//
+// $Id: Resource.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+
+package com.threerings.getdown.data;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import java.security.MessageDigest;
+
+import com.samskivert.util.StringUtil;
+
+/**
+ * Models a single file resource used by an {@link Application}.
+ */
+public class Resource
+{
+ /**
+ * Creates a resource with the supplied application base URL and
+ * resource path.
+ */
+ public Resource (URL appbase, File appdir, String path)
+ throws MalformedURLException
+ {
+ _path = path;
+ _remote = new URL(appbase, path);
+ _local = new File(appdir, path);
+ }
+
+ /**
+ * Returns the path associated with this resource.
+ */
+ public String getPath ()
+ {
+ return _path;
+ }
+
+ /**
+ * Computes the MD5 hash of this resource's underlying file.
+ * Note: This is both CPU and I/O intensive.
+ */
+ public String computeDigest (MessageDigest md)
+ throws IOException
+ {
+ md.reset();
+ byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
+ int read;
+ FileInputStream fin = new FileInputStream(_local);
+ while ((read = fin.read(buffer)) != -1) {
+ md.update(buffer, 0, read);
+ }
+ return StringUtil.hexlate(md.digest());
+ }
+
+ /**
+ * Returns a string representation of this instance.
+ */
+ public String toString ()
+ {
+ // return _path;
+ // return _netloc.toString();
+ return _local.getPath();
+ }
+
+ public static void main (String[] args)
+ {
+ try {
+ Resource rsrc = new Resource(
+ new URL("file:/"), new File("."), args[0]);
+ System.out.println("MD5: " + rsrc.computeDigest(
+ MessageDigest.getInstance("MD5")));
+ } catch (Exception e) {
+ e.printStackTrace(System.err);
+ }
+ }
+
+ protected String _path;
+ protected URL _remote;
+ protected File _local;
+
+ protected final static int DIGEST_BUFFER_SIZE = 5 * 1025;
+}
diff --git a/src/java/com/threerings/getdown/launcher/Getdown.java b/src/java/com/threerings/getdown/launcher/Getdown.java
new file mode 100644
index 0000000..1674aa7
--- /dev/null
+++ b/src/java/com/threerings/getdown/launcher/Getdown.java
@@ -0,0 +1,55 @@
+//
+// $Id: Getdown.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+
+package com.threerings.getdown.launcher;
+
+import java.io.File;
+
+import com.threerings.getdown.Log;
+import com.threerings.getdown.data.Application;
+
+/**
+ * Manages the main control for the Getdown application updater and
+ * deployment system.
+ */
+public class Getdown
+{
+ public Getdown (File appDir)
+ {
+ _app = new Application(appDir);
+ }
+
+ public void run ()
+ {
+ try {
+ _app.init();
+ } catch (Exception e) {
+ Log.logStackTrace(e);
+ }
+ }
+
+ public static void main (String[] args)
+ {
+ // ensure the proper parameters are passed
+ if (args.length != 1) {
+ System.err.println("Usage: java -jar getdown.jar app_dir");
+ System.exit(-1);
+ }
+
+ // ensure a valid directory was supplied
+ File appDir = new File(args[0]);
+ if (!appDir.exists() || !appDir.isDirectory()) {
+ Log.warning("Invalid app_dir '" + args[0] + "'.");
+ System.exit(-1);
+ }
+
+ try {
+ Getdown app = new Getdown(appDir);
+ app.run();
+ } catch (Exception e) {
+ Log.logStackTrace(e);
+ }
+ }
+
+ protected Application _app;
+}
diff --git a/src/java/com/threerings/getdown/tools/DigesterTask.java b/src/java/com/threerings/getdown/tools/DigesterTask.java
new file mode 100644
index 0000000..6834637
--- /dev/null
+++ b/src/java/com/threerings/getdown/tools/DigesterTask.java
@@ -0,0 +1,71 @@
+//
+// $Id: DigesterTask.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+
+package com.threerings.getdown.tools;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Task;
+
+import com.samskivert.util.CollectionUtil;
+
+import com.threerings.getdown.data.Application;
+import com.threerings.getdown.data.Digest;
+
+/**
+ * An ant task used to create a digest.txt for a Getdown
+ * application deployment.
+ */
+public class DigesterTask extends Task
+{
+ /**
+ * Sets the application directory.
+ */
+ public void setAppdir (File appdir)
+ {
+ _appdir = appdir;
+ }
+
+ /**
+ * Performs the actual work of the task.
+ */
+ public void execute () throws BuildException
+ {
+ // make sure appdir is set
+ if (_appdir == null) {
+ String errmsg = "Must specify the path to the application " +
+ "directory via the 'appdir' attribute.";
+ throw new BuildException(errmsg);
+ }
+ File target = new File(_appdir, Digest.DIGEST_FILE);
+ System.out.println("Generating digest file '" + target + "'...");
+
+ // create our application and instruct it to parse its business
+ Application app = new Application(_appdir);
+ try {
+ app.init();
+ } catch (IOException ioe) {
+ throw new BuildException("Error parsing getdown.txt: " +
+ ioe.getMessage(), ioe);
+ }
+
+ ArrayList rsrcs = new ArrayList();
+ rsrcs.add(app.getConfigResource());
+ CollectionUtil.addAll(rsrcs, app.getCodeResources().iterator());
+ CollectionUtil.addAll(rsrcs, app.getResources().iterator());
+
+ // now generate the digest file
+ try {
+ Digest.createDigest(rsrcs, target);
+ } catch (IOException ioe) {
+ throw new BuildException("Error creating digest: " +
+ ioe.getMessage(), ioe);
+ }
+ }
+
+ /** The application directory in which we're creating a digest file. */
+ protected File _appdir;
+}
diff --git a/src/java/com/threerings/getdown/util/ConfigUtil.java b/src/java/com/threerings/getdown/util/ConfigUtil.java
new file mode 100644
index 0000000..9a69aa4
--- /dev/null
+++ b/src/java/com/threerings/getdown/util/ConfigUtil.java
@@ -0,0 +1,119 @@
+//
+// $Id: ConfigUtil.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+
+package com.threerings.getdown.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import com.samskivert.util.StringUtil;
+
+/**
+ * Parses a file containing key/value pairs and returns a {@link HashMap}
+ * with the values. Keys may be repeated, in which case they will be made
+ * to reference an array of values.
+ */
+public class ConfigUtil
+{
+ /**
+ * Parses a configuration file containing key/value pairs. The file
+ * must be in the UTF-8 encoding.
+ *
+ * @return a list of String[] instances containing the
+ * key/value pairs in the order they were parsed from the file.
+ */
+ public static List parsePairs (File config)
+ throws IOException
+ {
+ ArrayList pairs = new ArrayList();
+
+ // parse our configuration file
+ BufferedReader bin = new BufferedReader(
+ new InputStreamReader(new FileInputStream(config), "UTF-8"));
+ String line = null;
+ while ((line = bin.readLine()) != null) {
+ // nix comments
+ int cidx = line.indexOf("#");
+ if (cidx != -1) {
+ line = line.substring(0, cidx);
+ }
+
+ // trim whitespace and skip blank lines
+ line = line.trim();
+ if (StringUtil.blank(line)) {
+ continue;
+ }
+
+ // parse our key/value pair
+ String[] pair = new String[2];
+ int eidx = line.indexOf("=");
+ if (eidx != -1) {
+ pair[0] = line.substring(0, eidx).trim();
+ pair[1] = line.substring(eidx+1).trim();
+ } else {
+ pair[0] = line;
+ pair[1] = "";
+ }
+
+ pairs.add(pair);
+ }
+
+ return pairs;
+ }
+
+ /**
+ * Parses a configuration file containing key/value pairs. The file
+ * must be in the UTF-8 encoding.
+ *
+ * @return a map from keys to values, where a value will be an array
+ * of strings if more than one key/value pair in the config file was
+ * associated with the same key.
+ */
+ public static HashMap parseConfig (File config)
+ throws IOException
+ {
+ List pairs = parsePairs(config);
+ HashMap data = new HashMap();
+
+ for (Iterator iter = pairs.iterator(); iter.hasNext(); ) {
+ String[] pair = (String[])iter.next();
+ Object value = data.get(pair[0]);
+ if (value == null) {
+ data.put(pair[0], pair[1]);
+ } else if (value instanceof String) {
+ data.put(pair[0], new String[] { (String)value, pair[1] });
+ } else if (value instanceof String[]) {
+ String[] values = (String[])value;
+ String[] nvalues = new String[values.length+1];
+ System.arraycopy(values, 0, nvalues, 0, values.length);
+ nvalues[values.length] = pair[1];
+ data.put(pair[0], nvalues);
+ }
+ }
+
+ return data;
+ }
+
+ /**
+ * Massages a single string into an array and leaves existing array
+ * values as is. Simplifies access to parameters that are expected to
+ * be arrays.
+ */
+ public static String[] getMultiValue (HashMap data, String name)
+ {
+ Object value = data.get(name);
+ if (value instanceof String) {
+ return new String[] { (String)value };
+ } else {
+ return (String[])value;
+ }
+ }
+}