2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
6 package com.threerings.getdown.data;
10 import java.nio.file.Files;
11 import java.nio.file.Paths;
12 import java.security.MessageDigest;
13 import java.util.Collections;
14 import java.util.Comparator;
15 import java.util.EnumSet;
16 import java.util.List;
17 import java.util.Locale;
18 import java.util.jar.JarEntry;
19 import java.util.jar.JarFile;
21 import org.apache.commons.compress.archivers.ArchiveInputStream;
22 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
23 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
25 import com.threerings.getdown.util.FileUtil;
26 import com.threerings.getdown.util.ProgressObserver;
27 import com.threerings.getdown.util.StringUtil;
29 import static com.threerings.getdown.Log.log;
32 * Models a single file resource used by an {@link Application}.
34 public class Resource implements Comparable<Resource>
36 /** Defines special attributes for resources. */
37 public static enum Attr {
38 /** Indicates that the resource should be unpacked. */
40 /** If present, when unpacking a resource, any directories created by the newly
41 * unpacked resource will first be cleared of files before unpacking. */
43 /** Indicates that the resource should be marked executable. */
45 /** Indicates that the resource should be downloaded before a UI is displayed. */
47 /** Indicates that the resource is a jar containing native libs. */
51 public static final EnumSet<Attr> NORMAL = EnumSet.noneOf(Attr.class);
52 public static final EnumSet<Attr> UNPACK = EnumSet.of(Attr.UNPACK);
53 public static final EnumSet<Attr> EXEC = EnumSet.of(Attr.EXEC);
54 public static final EnumSet<Attr> PRELOAD = EnumSet.of(Attr.PRELOAD);
55 public static final EnumSet<Attr> NATIVE = EnumSet.of(Attr.NATIVE);
58 * Computes the MD5 hash of the supplied file.
59 * @param version the version of the digest protocol to use.
61 public static String computeDigest (int version, File target, MessageDigest md,
66 byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
69 boolean isJar = isJar(target.getPath());
70 boolean isPacked200Jar = isPacked200Jar(target.getPath());
72 // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic
73 // manner to properly correlate jardiff patched jars with their unpatched originals
74 if (isJar || isPacked200Jar){
75 File tmpJarFile = null;
78 // if this is a compressed jar file, uncompress it to compute the jar file digest
80 tmpJarFile = new File(target.getPath() + ".tmp");
81 FileUtil.unpackPacked200Jar(target, tmpJarFile);
82 jar = new JarFile(tmpJarFile);
84 jar = new JarFile(target);
87 List<JarEntry> entries = Collections.list(jar.entries());
88 Collections.sort(entries, ENTRY_COMP);
91 for (JarEntry entry : entries) {
92 // old versions of the digest code skipped metadata
94 if (entry.getName().startsWith("META-INF")) {
95 updateProgress(obs, eidx, entries.size());
100 try (InputStream in = jar.getInputStream(entry)) {
101 while ((read = in.read(buffer)) != -1) {
102 md.update(buffer, 0, read);
106 updateProgress(obs, eidx, entries.size());
113 } catch (IOException ioe) {
114 log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe);
117 if (tmpJarFile != null) {
118 FileUtil.deleteHarder(tmpJarFile);
123 long totalSize = target.length(), position = 0L;
124 try (FileInputStream fin = new FileInputStream(target)) {
125 while ((read = fin.read(buffer)) != -1) {
126 md.update(buffer, 0, read);
128 updateProgress(obs, position, totalSize);
132 return StringUtil.hexlate(md.digest());
136 * Creates a resource with the supplied remote URL and local path.
138 public Resource (String path, URL remote, File local, EnumSet<Attr> attrs)
143 _localNew = new File(local.toString() + "_new");
144 String lpath = _local.getPath();
145 _marker = new File(lpath + "v");
148 _isTgz = isTgz(lpath);
149 _isJar = isJar(lpath);
150 _isPacked200Jar = isPacked200Jar(lpath);
151 boolean unpack = attrs.contains(Attr.UNPACK);
152 if (unpack && _isJar) {
153 _unpacked = _local.getParentFile();
154 } else if(unpack && _isTgz) {
155 _unpacked = _local.getParentFile();
156 } else if(unpack && _isPacked200Jar) {
157 String dotJar = ".jar", lname = _local.getName();
158 String uname = lname.substring(0, lname.lastIndexOf(dotJar) + dotJar.length());
159 _unpacked = new File(_local.getParent(), uname);
164 * Returns the path associated with this resource.
166 public String getPath ()
172 * Returns the local location of this resource.
174 public File getLocal ()
180 * Returns the location of the to-be-installed new version of this resource.
182 public File getLocalNew ()
188 * Returns the location of the unpacked resource.
190 public File getUnpacked ()
196 * Returns the final target of this resource, whether it has been unpacked or not.
198 public File getFinalTarget ()
200 return shouldUnpack() ? getUnpacked() : getLocal();
204 * Returns the remote location of this resource.
206 public URL getRemote ()
212 * Returns true if this resource should be unpacked as a part of the validation process.
214 public boolean shouldUnpack ()
216 return _attrs.contains(Attr.UNPACK) && !SysProps.noUnpack();
220 * Returns true if this resource should be pre-downloaded.
222 public boolean shouldPredownload ()
224 return _attrs.contains(Attr.PRELOAD);
228 * Returns true if this resource is a native lib jar.
230 public boolean isNative ()
232 return _attrs.contains(Attr.NATIVE);
236 * Computes the MD5 hash of this resource's underlying file.
237 * <em>Note:</em> This is both CPU and I/O intensive.
238 * @param version the version of the digest protocol to use.
240 public String computeDigest (int version, MessageDigest md, ProgressObserver obs)
244 if (_local.toString().toLowerCase(Locale.ROOT).endsWith(Application.CONFIG_FILE)) {
247 file = _localNew.exists() ? _localNew : _local;
249 return computeDigest(version, file, md, obs);
253 * Returns true if this resource has an associated "validated" marker
256 public boolean isMarkedValid ()
258 if (!_local.exists()) {
262 return _marker.exists();
266 * Creates a "validated" marker file for this resource to indicate
267 * that its MD5 hash has been computed and compared with the value in
270 * @throws IOException if we fail to create the marker file.
272 public void markAsValid ()
275 _marker.createNewFile();
279 * Removes any "validated" marker file associated with this resource.
281 public void clearMarker ()
283 if (_marker.exists() && !FileUtil.deleteHarder(_marker)) {
284 log.warning("Failed to erase marker file '" + _marker + "'.");
289 * Installs the {@code getLocalNew} version of this resource to {@code getLocal}.
290 * @param validate whether or not to mark the resource as valid after installing.
292 public void install (boolean validate) throws IOException {
293 File source = getLocalNew(), dest = getLocal();
294 log.info("- " + source);
295 if (!FileUtil.renameTo(source, dest)) {
296 throw new IOException("Failed to rename " + source + " to " + dest);
305 * Unpacks this resource file into the directory that contains it.
307 public void unpack () throws IOException
310 if (!_isJar && !_isPacked200Jar && !_isTgz) {
311 throw new IOException("Requested to unpack non-jar/tgz file '" + _local + "'.");
314 try (JarFile jar = new JarFile(_local)) {
315 FileUtil.unpackJar(jar, _unpacked, _attrs.contains(Attr.CLEAN));
318 try (InputStream fi = Files.newInputStream(_local.toPath());
319 InputStream bi = new BufferedInputStream(fi);
320 InputStream gzi = new GzipCompressorInputStream(bi);
321 TarArchiveInputStream tgz = new TarArchiveInputStream(gzi)) {
322 FileUtil.unpackTgz(tgz, _unpacked, _attrs.contains(Attr.CLEAN));
325 FileUtil.unpackPacked200Jar(_local, _unpacked);
330 * Applies this resources special attributes: unpacks this resource if needed, marks it as
331 * executable if needed.
333 public void applyAttrs () throws IOException {
334 if (shouldUnpack()) {
337 if (_attrs.contains(Attr.EXEC)) {
338 FileUtil.makeExecutable(_local);
343 * Wipes this resource file along with any "validated" marker file that may be associated with
349 if (_local.exists() && !FileUtil.deleteHarder(_local)) {
350 log.warning("Failed to erase resource '" + _local + "'.");
354 @Override public int compareTo (Resource other) {
355 return _path.compareTo(other._path);
358 @Override public boolean equals (Object other)
360 if (other instanceof Resource) {
361 return _path.equals(((Resource)other)._path);
367 @Override public int hashCode ()
369 return _path.hashCode();
372 @Override public String toString ()
377 /** Helper function to simplify the process of reporting progress. */
378 protected static void updateProgress (ProgressObserver obs, long pos, long total)
381 obs.progress((int)(100 * pos / total));
385 protected static boolean isJar (String path)
387 return path.endsWith(".jar") || path.endsWith(".jar_new");
390 protected static boolean isTgz (String path)
392 return path.endsWith(".tgz") || path.endsWith(".tgz_new");
395 protected static boolean isPacked200Jar (String path)
397 return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") ||
398 path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new");
401 protected String _path;
402 protected URL _remote;
403 protected File _local, _localNew, _marker, _unpacked;
404 protected EnumSet<Attr> _attrs;
405 protected boolean _isJar, _isPacked200Jar, _isTgz;
407 /** Used to sort the entries in a jar file. */
408 protected static final Comparator<JarEntry> ENTRY_COMP = new Comparator<JarEntry>() {
409 @Override public int compare (JarEntry e1, JarEntry e2) {
410 return e1.getName().compareTo(e2.getName());
414 protected static final int DIGEST_BUFFER_SIZE = 5 * 1025;