JAL-3130 adapted getdown src. attempt 2. first attempt failed due to cp'ed .git files
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / data / Resource.java
1 //
2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
5
6 package com.threerings.getdown.data;
7
8 import java.io.*;
9 import java.net.URL;
10 import java.security.MessageDigest;
11 import java.util.Collections;
12 import java.util.Comparator;
13 import java.util.EnumSet;
14 import java.util.List;
15 import java.util.Locale;
16 import java.util.jar.JarEntry;
17 import java.util.jar.JarFile;
18
19 import com.threerings.getdown.util.FileUtil;
20 import com.threerings.getdown.util.ProgressObserver;
21 import com.threerings.getdown.util.StringUtil;
22
23 import static com.threerings.getdown.Log.log;
24
25 /**
26  * Models a single file resource used by an {@link Application}.
27  */
28 public class Resource implements Comparable<Resource>
29 {
30     /** Defines special attributes for resources. */
31     public static enum Attr {
32         /** Indicates that the resource should be unpacked. */
33         UNPACK,
34         /** If present, when unpacking a resource, any directories created by the newly
35           * unpacked resource will first be cleared of files before unpacking. */
36         CLEAN,
37         /** Indicates that the resource should be marked executable. */
38         EXEC,
39         /** Indicates that the resource should be downloaded before a UI is displayed. */
40         PRELOAD,
41         /** Indicates that the resource is a jar containing native libs. */
42         NATIVE
43     };
44
45     public static final EnumSet<Attr> NORMAL  = EnumSet.noneOf(Attr.class);
46     public static final EnumSet<Attr> UNPACK  = EnumSet.of(Attr.UNPACK);
47     public static final EnumSet<Attr> EXEC    = EnumSet.of(Attr.EXEC);
48     public static final EnumSet<Attr> PRELOAD = EnumSet.of(Attr.PRELOAD);
49     public static final EnumSet<Attr> NATIVE  = EnumSet.of(Attr.NATIVE);
50
51     /**
52      * Computes the MD5 hash of the supplied file.
53      * @param version the version of the digest protocol to use.
54      */
55     public static String computeDigest (int version, File target, MessageDigest md,
56                                         ProgressObserver obs)
57         throws IOException
58     {
59         md.reset();
60         byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
61         int read;
62
63         boolean isJar = isJar(target.getPath());
64         boolean isPacked200Jar = isPacked200Jar(target.getPath());
65
66         // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic
67         // manner to properly correlate jardiff patched jars with their unpatched originals
68         if (isJar || isPacked200Jar){
69             File tmpJarFile = null;
70             JarFile jar = null;
71             try {
72                 // if this is a compressed jar file, uncompress it to compute the jar file digest
73                 if (isPacked200Jar){
74                     tmpJarFile = new File(target.getPath() + ".tmp");
75                     FileUtil.unpackPacked200Jar(target, tmpJarFile);
76                     jar = new JarFile(tmpJarFile);
77                 } else{
78                     jar = new JarFile(target);
79                 }
80
81                 List<JarEntry> entries = Collections.list(jar.entries());
82                 Collections.sort(entries, ENTRY_COMP);
83
84                 int eidx = 0;
85                 for (JarEntry entry : entries) {
86                     // old versions of the digest code skipped metadata
87                     if (version < 2) {
88                         if (entry.getName().startsWith("META-INF")) {
89                             updateProgress(obs, eidx, entries.size());
90                             continue;
91                         }
92                     }
93
94                     try (InputStream in = jar.getInputStream(entry)) {
95                         while ((read = in.read(buffer)) != -1) {
96                             md.update(buffer, 0, read);
97                         }
98                     }
99
100                     updateProgress(obs, eidx, entries.size());
101                 }
102
103             } finally {
104                 if (jar != null) {
105                     try {
106                         jar.close();
107                     } catch (IOException ioe) {
108                         log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe);
109                     }
110                 }
111                 if (tmpJarFile != null) {
112                     FileUtil.deleteHarder(tmpJarFile);
113                 }
114             }
115
116         } else {
117             long totalSize = target.length(), position = 0L;
118             try (FileInputStream fin = new FileInputStream(target)) {
119                 while ((read = fin.read(buffer)) != -1) {
120                     md.update(buffer, 0, read);
121                     position += read;
122                     updateProgress(obs, position, totalSize);
123                 }
124             }
125         }
126         return StringUtil.hexlate(md.digest());
127     }
128
129     /**
130      * Creates a resource with the supplied remote URL and local path.
131      */
132     public Resource (String path, URL remote, File local, EnumSet<Attr> attrs)
133     {
134         _path = path;
135         _remote = remote;
136         _local = local;
137         _localNew = new File(local.toString() + "_new");
138         String lpath = _local.getPath();
139         _marker = new File(lpath + "v");
140
141         _attrs = attrs;
142         _isJar = isJar(lpath);
143         _isPacked200Jar = isPacked200Jar(lpath);
144         boolean unpack = attrs.contains(Attr.UNPACK);
145         if (unpack && _isJar) {
146             _unpacked = _local.getParentFile();
147         } else if(unpack && _isPacked200Jar) {
148             String dotJar = ".jar", lname = _local.getName();
149             String uname = lname.substring(0, lname.lastIndexOf(dotJar) + dotJar.length());
150             _unpacked = new File(_local.getParent(), uname);
151         }
152     }
153
154     /**
155      * Returns the path associated with this resource.
156      */
157     public String getPath ()
158     {
159         return _path;
160     }
161
162     /**
163      * Returns the local location of this resource.
164      */
165     public File getLocal ()
166     {
167         return _local;
168     }
169
170     /**
171      * Returns the location of the to-be-installed new version of this resource.
172      */
173     public File getLocalNew ()
174     {
175         return _localNew;
176     }
177
178     /**
179      *  Returns the location of the unpacked resource.
180      */
181     public File getUnpacked ()
182     {
183         return _unpacked;
184     }
185
186     /**
187      *  Returns the final target of this resource, whether it has been unpacked or not.
188      */
189     public File getFinalTarget ()
190     {
191         return shouldUnpack() ? getUnpacked() : getLocal();
192     }
193
194     /**
195      * Returns the remote location of this resource.
196      */
197     public URL getRemote ()
198     {
199         return _remote;
200     }
201
202     /**
203      * Returns true if this resource should be unpacked as a part of the validation process.
204      */
205     public boolean shouldUnpack ()
206     {
207         return _attrs.contains(Attr.UNPACK) && !SysProps.noUnpack();
208     }
209
210     /**
211      * Returns true if this resource should be pre-downloaded.
212      */
213     public boolean shouldPredownload ()
214     {
215         return _attrs.contains(Attr.PRELOAD);
216     }
217
218     /**
219      * Returns true if this resource is a native lib jar.
220      */
221     public boolean isNative ()
222     {
223         return _attrs.contains(Attr.NATIVE);
224     }
225
226     /**
227      * Computes the MD5 hash of this resource's underlying file.
228      * <em>Note:</em> This is both CPU and I/O intensive.
229      * @param version the version of the digest protocol to use.
230      */
231     public String computeDigest (int version, MessageDigest md, ProgressObserver obs)
232         throws IOException
233     {
234         File file;
235         if (_local.toString().toLowerCase(Locale.ROOT).endsWith(Application.CONFIG_FILE)) {
236             file = _local;
237         } else {
238             file = _localNew.exists() ? _localNew : _local;
239         }
240         return computeDigest(version, file, md, obs);
241     }
242
243     /**
244      * Returns true if this resource has an associated "validated" marker
245      * file.
246      */
247     public boolean isMarkedValid ()
248     {
249         if (!_local.exists()) {
250             clearMarker();
251             return false;
252         }
253         return _marker.exists();
254     }
255
256     /**
257      * Creates a "validated" marker file for this resource to indicate
258      * that its MD5 hash has been computed and compared with the value in
259      * the digest file.
260      *
261      * @throws IOException if we fail to create the marker file.
262      */
263     public void markAsValid ()
264         throws IOException
265     {
266         _marker.createNewFile();
267     }
268
269     /**
270      * Removes any "validated" marker file associated with this resource.
271      */
272     public void clearMarker ()
273     {
274         if (_marker.exists() && !FileUtil.deleteHarder(_marker)) {
275             log.warning("Failed to erase marker file '" + _marker + "'.");
276         }
277     }
278
279     /**
280      * Installs the {@code getLocalNew} version of this resource to {@code getLocal}.
281      * @param validate whether or not to mark the resource as valid after installing.
282      */
283     public void install (boolean validate) throws IOException {
284         File source = getLocalNew(), dest = getLocal();
285         log.info("- " + source);
286         if (!FileUtil.renameTo(source, dest)) {
287             throw new IOException("Failed to rename " + source + " to " + dest);
288         }
289         applyAttrs();
290         if (validate) {
291             markAsValid();
292         }
293     }
294
295     /**
296      * Unpacks this resource file into the directory that contains it.
297      */
298     public void unpack () throws IOException
299     {
300         // sanity check
301         if (!_isJar && !_isPacked200Jar) {
302             throw new IOException("Requested to unpack non-jar file '" + _local + "'.");
303         }
304         if (_isJar) {
305             try (JarFile jar = new JarFile(_local)) {
306                 FileUtil.unpackJar(jar, _unpacked, _attrs.contains(Attr.CLEAN));
307             }
308         } else {
309             FileUtil.unpackPacked200Jar(_local, _unpacked);
310         }
311     }
312
313     /**
314      * Applies this resources special attributes: unpacks this resource if needed, marks it as
315      * executable if needed.
316      */
317     public void applyAttrs () throws IOException {
318         if (shouldUnpack()) {
319             unpack();
320         }
321         if (_attrs.contains(Attr.EXEC)) {
322             FileUtil.makeExecutable(_local);
323         }
324     }
325
326     /**
327      * Wipes this resource file along with any "validated" marker file that may be associated with
328      * it.
329      */
330     public void erase ()
331     {
332         clearMarker();
333         if (_local.exists() && !FileUtil.deleteHarder(_local)) {
334             log.warning("Failed to erase resource '" + _local + "'.");
335         }
336     }
337
338     @Override public int compareTo (Resource other) {
339         return _path.compareTo(other._path);
340     }
341
342     @Override public boolean equals (Object other)
343     {
344         if (other instanceof Resource) {
345             return _path.equals(((Resource)other)._path);
346         } else {
347             return false;
348         }
349     }
350
351     @Override public int hashCode ()
352     {
353         return _path.hashCode();
354     }
355
356     @Override public String toString ()
357     {
358         return _path;
359     }
360
361     /** Helper function to simplify the process of reporting progress. */
362     protected static void updateProgress (ProgressObserver obs, long pos, long total)
363     {
364         if (obs != null) {
365             obs.progress((int)(100 * pos / total));
366         }
367     }
368
369     protected static boolean isJar (String path)
370     {
371         return path.endsWith(".jar") || path.endsWith(".jar_new");
372     }
373
374     protected static boolean isPacked200Jar (String path)
375     {
376         return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") ||
377             path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new");
378     }
379
380     protected String _path;
381     protected URL _remote;
382     protected File _local, _localNew, _marker, _unpacked;
383     protected EnumSet<Attr> _attrs;
384     protected boolean _isJar, _isPacked200Jar;
385
386     /** Used to sort the entries in a jar file. */
387     protected static final Comparator<JarEntry> ENTRY_COMP = new Comparator<JarEntry>() {
388         @Override public int compare (JarEntry e1, JarEntry e2) {
389             return e1.getName().compareTo(e2.getName());
390         }
391     };
392
393     protected static final int DIGEST_BUFFER_SIZE = 5 * 1025;
394 }