Create a separate digest.txt file for each "protocol version".
Right now there's only two versions of the digester protocol, so I just always generate both of them. If we evolve the protocol further, we could conceivably allow you to specify a "minimum" protocol version, once you were confident that all of your clients had upgraded to a version of Getdown that used that minimum version.
This commit is contained in:
@@ -904,7 +904,7 @@ public class Application
|
||||
// now re-download our control files; we download the digest first so that if it fails,
|
||||
// our config file will still reference the old version and re-running the updater will
|
||||
// start the whole process over again
|
||||
downloadDigestFile();
|
||||
downloadDigestFiles();
|
||||
downloadConfigFile();
|
||||
|
||||
} catch (IOException ex) {
|
||||
@@ -1171,7 +1171,7 @@ public class Application
|
||||
String olddig = (_digest == null) ? "" : _digest.getMetaDigest();
|
||||
try {
|
||||
status.updateStatus("m.checking");
|
||||
downloadDigestFile();
|
||||
downloadDigestFiles();
|
||||
_digest = new Digest(_appdir);
|
||||
if (!olddig.equals(_digest.getMetaDigest())) {
|
||||
log.info("Unversioned digest changed. Revalidating...");
|
||||
@@ -1189,7 +1189,7 @@ public class Application
|
||||
// exceptions to propagate up to the caller as there is nothing else we can do
|
||||
if (_digest == null) {
|
||||
status.updateStatus("m.updating_metadata");
|
||||
downloadDigestFile();
|
||||
downloadDigestFiles();
|
||||
_digest = new Digest(_appdir);
|
||||
}
|
||||
|
||||
@@ -1200,7 +1200,7 @@ public class Application
|
||||
// attempt to redownload both of our metadata files; again we pass errors up to our
|
||||
// caller because there's nothing we can do to automatically recover
|
||||
downloadConfigFile();
|
||||
downloadDigestFile();
|
||||
downloadDigestFiles();
|
||||
_digest = new Digest(_appdir);
|
||||
// revalidate everything if we end up downloading new metadata
|
||||
clearValidationMarkers();
|
||||
@@ -1459,13 +1459,15 @@ public class Application
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a copy of Digest.DIGEST_FILE and validates its signature.
|
||||
* Downloads the digest files and validates their signature.
|
||||
* @throws IOException
|
||||
*/
|
||||
protected void downloadDigestFile ()
|
||||
protected void downloadDigestFiles ()
|
||||
throws IOException
|
||||
{
|
||||
downloadControlFile(Digest.DIGEST_FILE, true);
|
||||
for (int version = 1; version <= Digest.VERSION; version++) {
|
||||
downloadControlFile(Digest.digestFile(version), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,21 +30,80 @@ import static com.threerings.getdown.Log.log;
|
||||
*/
|
||||
public class Digest
|
||||
{
|
||||
/** The name of our MD5 digest file. */
|
||||
public static final String DIGEST_FILE = "digest.txt";
|
||||
/** The current version of the digest protocol. */
|
||||
public static final int VERSION = 2;
|
||||
|
||||
/**
|
||||
* Returns the name of the digest file for the specified protocol version.
|
||||
*/
|
||||
public static String digestFile (int version) {
|
||||
String infix = version > 1 ? String.valueOf(version) : "";
|
||||
return FILE_NAME + infix + FILE_SUFFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a digest file at the specified location using the supplied list of resources.
|
||||
* @param version the version of the digest protocol to use.
|
||||
*/
|
||||
public static void createDigest (int version, List<Resource> resources, File output)
|
||||
throws IOException
|
||||
{
|
||||
MessageDigest md = getMessageDigest();
|
||||
StringBuilder data = new StringBuilder();
|
||||
PrintWriter pout = null;
|
||||
try {
|
||||
pout = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output), "UTF-8"));
|
||||
|
||||
// compute and append the MD5 digest of each resource in the list
|
||||
for (Resource rsrc : resources) {
|
||||
String path = rsrc.getPath();
|
||||
try {
|
||||
String digest = rsrc.computeDigest(version, md, null);
|
||||
note(data, path, digest);
|
||||
pout.println(path + " = " + digest);
|
||||
} catch (Throwable t) {
|
||||
throw (IOException) new IOException(
|
||||
"Error computing digest for: " + rsrc).initCause(t);
|
||||
}
|
||||
}
|
||||
|
||||
// finally compute and append the digest for the file contents
|
||||
md.reset();
|
||||
byte[] contents = data.toString().getBytes("UTF-8");
|
||||
String filename = digestFile(version);
|
||||
pout.println(filename + " = " + StringUtil.hexlate(md.digest(contents)));
|
||||
|
||||
} finally {
|
||||
StreamUtil.close(pout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains an appropriate message digest instance for use by the Getdown system.
|
||||
*/
|
||||
public static MessageDigest getMessageDigest ()
|
||||
{
|
||||
try {
|
||||
return MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
throw new RuntimeException("JVM does not support MD5. Gurp!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a digest instance which will parse and validate the <code>digest.txt</code> in the
|
||||
* supplied application directory.
|
||||
* @param version the version of the digest protocol to use.
|
||||
*/
|
||||
public Digest (File appdir)
|
||||
throws IOException
|
||||
{
|
||||
// parse and validate our digest file contents
|
||||
String filename = digestFile(VERSION);
|
||||
StringBuilder data = new StringBuilder();
|
||||
File dfile = new File(appdir, DIGEST_FILE);
|
||||
File dfile = new File(appdir, filename);
|
||||
for (String[] pair : ConfigUtil.parsePairs(dfile, false)) {
|
||||
if (pair[0].equals(DIGEST_FILE)) {
|
||||
if (pair[0].equals(filename)) {
|
||||
_metaDigest = pair[1];
|
||||
break;
|
||||
}
|
||||
@@ -80,7 +139,7 @@ public class Digest
|
||||
public boolean validateResource (Resource resource, ProgressObserver obs)
|
||||
{
|
||||
try {
|
||||
String cmd5 = resource.computeDigest(getMessageDigest(), obs);
|
||||
String cmd5 = resource.computeDigest(VERSION, getMessageDigest(), obs);
|
||||
String emd5 = _digests.get(resource.getPath());
|
||||
if (cmd5.equals(emd5)) {
|
||||
return true;
|
||||
@@ -101,53 +160,6 @@ public class Digest
|
||||
return _digests.get(resource.getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a digest file at the specified location using the supplied list of resources.
|
||||
*/
|
||||
public static void createDigest (List<Resource> resources, File output)
|
||||
throws IOException
|
||||
{
|
||||
MessageDigest md = getMessageDigest();
|
||||
StringBuilder data = new StringBuilder();
|
||||
PrintWriter pout = null;
|
||||
try {
|
||||
pout = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output), "UTF-8"));
|
||||
|
||||
// compute and append the MD5 digest of each resource in the list
|
||||
for (Resource rsrc : resources) {
|
||||
String path = rsrc.getPath();
|
||||
try {
|
||||
String digest = rsrc.computeDigest(md, null);
|
||||
note(data, path, digest);
|
||||
pout.println(path + " = " + digest);
|
||||
} catch (Throwable t) {
|
||||
throw (IOException) new IOException(
|
||||
"Error computing digest for: " + rsrc).initCause(t);
|
||||
}
|
||||
}
|
||||
|
||||
// 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)));
|
||||
|
||||
} finally {
|
||||
StreamUtil.close(pout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains an appropriate message digest instance for use by the Getdown system.
|
||||
*/
|
||||
public static MessageDigest getMessageDigest ()
|
||||
{
|
||||
try {
|
||||
return MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
throw new RuntimeException("JVM does not support MD5. Gurp!");
|
||||
}
|
||||
}
|
||||
|
||||
/** Used by {@link #createDigest} and {@link Digest}. */
|
||||
protected static void note (StringBuilder data, String path, String digest)
|
||||
{
|
||||
@@ -156,4 +168,7 @@ public class Digest
|
||||
|
||||
protected HashMap<String, String> _digests = new HashMap<String, String>();
|
||||
protected String _metaDigest = "";
|
||||
|
||||
protected static final String FILE_NAME = "digest";
|
||||
protected static final String FILE_SUFFIX = ".txt";
|
||||
}
|
||||
|
||||
@@ -27,6 +27,91 @@ import static com.threerings.getdown.Log.log;
|
||||
*/
|
||||
public class Resource
|
||||
{
|
||||
/**
|
||||
* Computes the MD5 hash of the supplied file.
|
||||
* @param version the version of the digest protocol to use.
|
||||
*/
|
||||
public static String computeDigest (int version, File target, MessageDigest md,
|
||||
ProgressObserver obs)
|
||||
throws IOException
|
||||
{
|
||||
md.reset();
|
||||
byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
|
||||
int read;
|
||||
|
||||
boolean isJar = isJar(target.getPath());
|
||||
boolean isPacked200Jar = isPacked200Jar(target.getPath());
|
||||
|
||||
// if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic
|
||||
// manner to properly correlate jardiff patched jars with their unpatched originals
|
||||
if (isJar || isPacked200Jar){
|
||||
File tmpJarFile = null;
|
||||
JarFile jar = null;
|
||||
try {
|
||||
// if this is a compressed jar file, uncompress it to compute the jar file digest
|
||||
if (isPacked200Jar){
|
||||
tmpJarFile = new File(target.getPath() + ".tmp");
|
||||
FileUtil.unpackPacked200Jar(target, tmpJarFile);
|
||||
jar = new JarFile(tmpJarFile);
|
||||
} else{
|
||||
jar = new JarFile(target);
|
||||
}
|
||||
|
||||
List<JarEntry> entries = Collections.list(jar.entries());
|
||||
Collections.sort(entries, ENTRY_COMP);
|
||||
|
||||
int eidx = 0;
|
||||
for (JarEntry entry : entries) {
|
||||
// old versions of the digest code skipped metadata
|
||||
if (version < 2) {
|
||||
if (entry.getName().startsWith("META-INF")) {
|
||||
updateProgress(obs, eidx, entries.size());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = jar.getInputStream(entry);
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
md.update(buffer, 0, read);
|
||||
}
|
||||
} finally {
|
||||
StreamUtil.close(in);
|
||||
}
|
||||
updateProgress(obs, eidx, entries.size());
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (jar != null) {
|
||||
try {
|
||||
jar.close();
|
||||
} catch (IOException ioe) {
|
||||
log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe);
|
||||
}
|
||||
}
|
||||
if (tmpJarFile != null) {
|
||||
tmpJarFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
long totalSize = target.length(), position = 0L;
|
||||
FileInputStream fin = null;
|
||||
try {
|
||||
fin = new FileInputStream(target);
|
||||
while ((read = fin.read(buffer)) != -1) {
|
||||
md.update(buffer, 0, read);
|
||||
position += read;
|
||||
updateProgress(obs, position, totalSize);
|
||||
}
|
||||
} finally {
|
||||
StreamUtil.close(fin);
|
||||
}
|
||||
}
|
||||
return StringUtil.hexlate(md.digest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a resource with the supplied remote URL and local path.
|
||||
*/
|
||||
@@ -111,8 +196,10 @@ public class Resource
|
||||
/**
|
||||
* Computes the MD5 hash of this resource's underlying file.
|
||||
* <em>Note:</em> This is both CPU and I/O intensive.
|
||||
* @param version the version of the digest protocol to use.
|
||||
*/
|
||||
public String computeDigest (MessageDigest md, ProgressObserver obs) throws IOException
|
||||
public String computeDigest (int version, MessageDigest md, ProgressObserver obs)
|
||||
throws IOException
|
||||
{
|
||||
File file;
|
||||
if (_local.toString().toLowerCase().endsWith(Application.CONFIG_FILE)) {
|
||||
@@ -120,7 +207,7 @@ public class Resource
|
||||
} else {
|
||||
file = _localNew.exists() ? _localNew : _local;
|
||||
}
|
||||
return computeDigest(file, md, obs);
|
||||
return computeDigest(version, file, md, obs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,81 +300,6 @@ public class Resource
|
||||
return _path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the MD5 hash of the supplied file.
|
||||
*/
|
||||
public static String computeDigest (File target, MessageDigest md, ProgressObserver obs)
|
||||
throws IOException
|
||||
{
|
||||
md.reset();
|
||||
byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
|
||||
int read;
|
||||
|
||||
boolean isJar = isJar(target.getPath());
|
||||
boolean isPacked200Jar = isPacked200Jar(target.getPath());
|
||||
|
||||
// if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic
|
||||
// manner to properly correlate jardiff patched jars with their unpatched originals
|
||||
if (isJar || isPacked200Jar){
|
||||
File tmpJarFile = null;
|
||||
JarFile jar = null;
|
||||
try {
|
||||
// if this is a compressed jar file, uncompress it to compute the jar file digest
|
||||
if (isPacked200Jar){
|
||||
tmpJarFile = new File(target.getPath() + ".tmp");
|
||||
FileUtil.unpackPacked200Jar(target, tmpJarFile);
|
||||
jar = new JarFile(tmpJarFile);
|
||||
} else{
|
||||
jar = new JarFile(target);
|
||||
}
|
||||
|
||||
List<JarEntry> entries = Collections.list(jar.entries());
|
||||
Collections.sort(entries, ENTRY_COMP);
|
||||
|
||||
int eidx = 0;
|
||||
for (JarEntry entry : entries) {
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = jar.getInputStream(entry);
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
md.update(buffer, 0, read);
|
||||
}
|
||||
} finally {
|
||||
StreamUtil.close(in);
|
||||
}
|
||||
updateProgress(obs, eidx, entries.size());
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (jar != null) {
|
||||
try {
|
||||
jar.close();
|
||||
} catch (IOException ioe) {
|
||||
log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe);
|
||||
}
|
||||
}
|
||||
if (tmpJarFile != null) {
|
||||
tmpJarFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
long totalSize = target.length(), position = 0L;
|
||||
FileInputStream fin = null;
|
||||
try {
|
||||
fin = new FileInputStream(target);
|
||||
while ((read = fin.read(buffer)) != -1) {
|
||||
md.update(buffer, 0, read);
|
||||
position += read;
|
||||
updateProgress(obs, position, totalSize);
|
||||
}
|
||||
} finally {
|
||||
StreamUtil.close(fin);
|
||||
}
|
||||
}
|
||||
return StringUtil.hexlate(md.digest());
|
||||
}
|
||||
|
||||
/** Helper function to simplify the process of reporting progress. */
|
||||
protected static void updateProgress (ProgressObserver obs, long pos, long total)
|
||||
{
|
||||
|
||||
@@ -107,8 +107,9 @@ public class Differ
|
||||
Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx);
|
||||
if (orsrc != null) {
|
||||
// first see if they are the same
|
||||
String odig = orsrc.computeDigest(md, null);
|
||||
String ndig = rsrc.computeDigest(md, null);
|
||||
int version = Digest.VERSION;
|
||||
String odig = orsrc.computeDigest(version, md, null);
|
||||
String ndig = rsrc.computeDigest(version, md, null);
|
||||
if (odig.equals(ndig)) {
|
||||
if (verbose) {
|
||||
System.out.println("Unchanged: " + rsrc.getPath());
|
||||
|
||||
@@ -36,24 +36,40 @@ public class Digester
|
||||
public static void main (String[] args)
|
||||
throws IOException, GeneralSecurityException
|
||||
{
|
||||
if (args.length != 1 && args.length != 4) {
|
||||
switch (args.length) {
|
||||
case 1:
|
||||
createDigests(new File(args[0]), null, null, null);
|
||||
break;
|
||||
case 4:
|
||||
createDigests(new File(args[0]), new File(args[1]), args[2], args[3]);
|
||||
break;
|
||||
default:
|
||||
System.err.println("Usage: Digester app_dir [keystore_path password alias]");
|
||||
System.exit(255);
|
||||
}
|
||||
}
|
||||
|
||||
createDigest(new File(args[0]));
|
||||
if (args.length == 4) {
|
||||
signDigest(new File(args[0]), new File(args[1]), args[2], args[3]);
|
||||
/**
|
||||
* Creates digest file(s) and optionally signs them if {@code keystore} is not null.
|
||||
*/
|
||||
public static void createDigests (File appdir, File keystore, String password, String alias)
|
||||
throws IOException, GeneralSecurityException
|
||||
{
|
||||
for (int version = 1; version <= Digest.VERSION; version++) {
|
||||
createDigest(version, appdir);
|
||||
if (keystore != null) {
|
||||
signDigest(version, appdir, keystore, password, alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a digest file in the specified application directory.
|
||||
*/
|
||||
public static void createDigest (File appdir)
|
||||
public static void createDigest (int version, File appdir)
|
||||
throws IOException
|
||||
{
|
||||
File target = new File(appdir, Digest.DIGEST_FILE);
|
||||
File target = new File(appdir, Digest.digestFile(version));
|
||||
System.out.println("Generating digest file '" + target + "'...");
|
||||
|
||||
// create our application and instruct it to parse its business
|
||||
@@ -70,17 +86,19 @@ public class Digester
|
||||
}
|
||||
|
||||
// now generate the digest file
|
||||
Digest.createDigest(rsrcs, target);
|
||||
Digest.createDigest(version, rsrcs, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a digest file in the specified application directory.
|
||||
*/
|
||||
public static void signDigest (File appdir, File storePath, String storePass, String storeAlias)
|
||||
public static void signDigest (int version, File appdir,
|
||||
File storePath, String storePass, String storeAlias)
|
||||
throws IOException, GeneralSecurityException
|
||||
{
|
||||
File inputFile = new File(appdir, Digest.DIGEST_FILE);
|
||||
File signatureFile = new File(appdir, Digest.DIGEST_FILE + Application.SIGNATURE_SUFFIX);
|
||||
String filename = Digest.digestFile(version);
|
||||
File inputFile = new File(appdir, filename);
|
||||
File signatureFile = new File(appdir, filename + Application.SIGNATURE_SUFFIX);
|
||||
|
||||
FileInputStream storeInput = null, dataInput = null;
|
||||
FileOutputStream signatureOutput = null;
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.security.GeneralSecurityException;
|
||||
import org.apache.tools.ant.BuildException;
|
||||
import org.apache.tools.ant.Task;
|
||||
|
||||
import com.threerings.getdown.data.Digest;
|
||||
|
||||
/**
|
||||
* An ant task used to create a <code>digest.txt</code> for a Getdown
|
||||
* application deployment.
|
||||
@@ -70,10 +72,7 @@ public class DigesterTask extends Task
|
||||
}
|
||||
|
||||
try {
|
||||
Digester.createDigest(_appdir);
|
||||
if (_storepath != null) {
|
||||
Digester.signDigest(_appdir, _storepath, _storepass, _storealias);
|
||||
}
|
||||
Digester.createDigests(_appdir, _storepath, _storepass, _storealias);
|
||||
} catch (IOException ioe) {
|
||||
throw new BuildException("Error creating digest: " + ioe.getMessage(), ioe);
|
||||
} catch (GeneralSecurityException gse) {
|
||||
|
||||
Reference in New Issue
Block a user