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