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