JAL-3252 some debugging
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / util / FileUtil.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.util;
7
8 import java.io.*;
9 import java.nio.file.Files;
10 import java.nio.file.Paths;
11 import java.util.*;
12 import java.util.jar.*;
13 import java.util.zip.GZIPInputStream;
14
15 import org.apache.commons.compress.archivers.ArchiveEntry;
16 import org.apache.commons.compress.archivers.ArchiveInputStream;
17 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
18 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
19
20 import com.threerings.getdown.util.StreamUtil;
21 import com.threerings.getdown.Log;
22 import static com.threerings.getdown.Log.log;
23
24 /**
25  * File related utilities.
26  */
27 public class FileUtil
28 {
29     /**
30      * Gets the specified source file to the specified destination file by hook or crook. Windows
31      * has all sorts of problems which we work around in this method.
32      *
33      * @return true if we managed to get the job done, false otherwise.
34      */
35     public static boolean renameTo (File source, File dest)
36     {
37         // if we're on a civilized operating system we may be able to simple rename it
38         if (source.renameTo(dest)) {
39             return true;
40         }
41
42         // fall back to trying to rename the old file out of the way, rename the new file into
43         // place and then delete the old file
44         if (dest.exists()) {
45             File temp = new File(dest.getPath() + "_old");
46             if (temp.exists() && !deleteHarder(temp)) {
47                 log.warning("Failed to delete old intermediate file " + temp + ".");
48                 // the subsequent code will probably fail
49             }
50             if (dest.renameTo(temp) && source.renameTo(dest)) {
51                 if (!deleteHarder(temp)) {
52                     log.warning("Failed to delete intermediate file " + temp + ".");
53                 }
54                 return true;
55             }
56         }
57
58         // as a last resort, try copying the old data over the new
59         try {
60             copy(source, dest);
61         } catch (IOException ioe) {
62             log.warning("Failed to copy " + source + " to " + dest + ": " + ioe);
63             return false;
64         }
65
66         if (!deleteHarder(source)) {
67             log.warning("Failed to delete " + source + " after brute force copy to " + dest + ".");
68         }
69         return true;
70     }
71
72     /**
73      * "Tries harder" to delete {@code file} than just calling {@code delete} on it. Presently this
74      * just means "try a second time if the first time fails, and if that fails then try to delete
75      * when the virtual machine terminates." On Windows Vista, sometimes deletes fail but then
76      * succeed if you just try again. Given that delete failure is a rare occurrence, we can
77      * implement this hacky workaround without any negative consequences for normal behavior.
78      */
79     public static boolean deleteHarder (File file) {
80         // if at first you don't succeed... try, try again
81         boolean deleted = (file.delete() || file.delete());
82         if (!deleted) {
83             file.deleteOnExit();
84         }
85         return deleted;
86     }
87
88     /**
89      * Force deletes {@code file} and all of its children recursively using {@link #deleteHarder}.
90      * Note that some children may still be deleted even if {@code false} is returned. Also, since
91      * {@link #deleteHarder} is used, the {@code file} could be deleted once the jvm exits even if
92      * {@code false} is returned.
93      *
94      * @param file file to delete.
95      * @return true iff {@code file} was successfully deleted.
96      */
97     public static boolean deleteDirHarder (File file) {
98         if (file.isDirectory()) {
99             for (File child : file.listFiles()) {
100                 deleteDirHarder(child);
101             }
102         }
103         return deleteHarder(file);
104     }
105
106     /**
107      * Reads the contents of the supplied input stream into a list of lines. Closes the reader on
108      * successful or failed completion.
109      */
110     public static List<String> readLines (Reader in)
111         throws IOException
112     {
113         List<String> lines = new ArrayList<>();
114         try (BufferedReader bin = new BufferedReader(in)) {
115             for (String line = null; (line = bin.readLine()) != null; lines.add(line)) {}
116         }
117         return lines;
118     }
119
120     /**
121      * Unpacks the specified jar file into the specified target directory.
122      * @param cleanExistingDirs if true, all files in all directories contained in {@code jar} will
123      * be deleted prior to unpacking the jar.
124      */
125     public static void unpackJar (JarFile jar, File target, boolean cleanExistingDirs)
126         throws IOException
127     {
128         if (cleanExistingDirs) {
129             Enumeration<?> entries = jar.entries();
130             while (entries.hasMoreElements()) {
131                 JarEntry entry = (JarEntry)entries.nextElement();
132                 if (entry.isDirectory()) {
133                     File efile = new File(target, entry.getName());
134                     if (efile.exists()) {
135                         for (File f : efile.listFiles()) {
136                             if (!f.isDirectory())
137                             f.delete();
138                         }
139                     }
140                 }
141             }
142         }
143
144         Enumeration<?> entries = jar.entries();
145         while (entries.hasMoreElements()) {
146             JarEntry entry = (JarEntry)entries.nextElement();
147             File efile = new File(target, entry.getName());
148
149             // if we're unpacking a normal jar file, it will have special path
150             // entries that allow us to create our directories first
151             if (entry.isDirectory()) {
152                 if (!efile.exists() && !efile.mkdir()) {
153                     log.warning("Failed to create jar entry path", "jar", jar, "entry", entry);
154                 }
155                 continue;
156             }
157
158             // but some do not, so we want to ensure that our directories exist
159             // prior to getting down and funky
160             File parent = new File(efile.getParent());
161             if (!parent.exists() && !parent.mkdirs()) {
162                 log.warning("Failed to create jar entry parent", "jar", jar, "parent", parent);
163                 continue;
164             }
165
166             try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
167                 InputStream jin = jar.getInputStream(entry)) {
168                 StreamUtil.copy(jin, fout);
169             } catch (Exception e) {
170                 throw new IOException(
171                     Log.format("Failure unpacking", "jar", jar, "entry", efile), e);
172             }
173         }
174     }
175
176     /**
177      * Unpacks the specified tgz file into the specified target directory.
178      * @param cleanExistingDirs if true, all files in all directories contained in {@code tgz} will
179      * be deleted prior to unpacking the tgz.
180      */
181     public static void unpackTgz (TarArchiveInputStream tgz, File target, boolean cleanExistingDirs)
182         throws IOException
183     {
184                 TarArchiveEntry entry = tgz.getNextTarEntry();
185                 while (entry != null) {
186             // sanitize the entry name
187                         String entryName = entry.getName();
188             log.info("### ENTRYNAME="+entryName);
189                         if (entryName.startsWith(File.separator))
190                         {
191                                 entryName = entryName.substring(File.separator.length());
192                         }
193             File efile = new File(target, entryName);
194             log.info("###1 ENTRYNAME="+entryName);
195
196             // if we're unpacking a normal tgz file, it will have special path
197             // entries that allow us to create our directories first
198             if (entry.isDirectory()) {
199             log.info("###2 ENTRYNAME="+entryName);
200                 log.info("Directory '"+entryName+"', cleanExistingDirs="+cleanExistingDirs);
201                                 if (cleanExistingDirs) {
202                     if (efile.exists()) {
203             log.info("###3 ENTRYNAME="+entryName);
204                         for (File f : efile.listFiles()) {
205             log.info("###4 ENTRYNAME="+entryName);
206                             if (!f.isDirectory()) {
207                                 log.info("Attempting to delete "+f.getName());
208                                 f.delete();
209                             }
210                         }
211                     }
212                                 }
213                                 
214             log.info("###5 ENTRYNAME="+entryName);
215                 if (!efile.exists() && !efile.mkdir()) {
216                     log.warning("Failed to create tgz entry path", "tgz", tgz, "entry", entry);
217                 }
218             entry = tgz.getNextTarEntry();
219             log.info("###5a ENTRYNAME="+entryName);
220                 continue;
221             }
222
223             // but some do not, so we want to ensure that our directories exist
224             // prior to getting down and funky
225             File parent = new File(efile.getParent());
226             if (!parent.exists() && !parent.mkdirs()) {
227                 log.warning("Failed to create tgz entry parent", "tgz", tgz, "parent", parent);
228             entry = tgz.getNextTarEntry();
229             log.info("###7a ENTRYNAME="+entryName);
230                 continue;
231             }
232
233             log.info("###6 ENTRYNAME="+entryName);
234             if (entry.isLink())
235             {
236             log.info("###7 ENTRYNAME="+entryName);
237                 log.info("Creating hard link "+efile.getName()+" -> "+entry.getLinkName());
238                 Files.createLink(efile.toPath(), Paths.get(entry.getLinkName()));
239             log.info("###7a ENTRYNAME="+entryName);
240             entry = tgz.getNextTarEntry();
241             log.info("###7a ENTRYNAME="+entryName);
242                 continue;
243             }
244
245             log.info("###8 ENTRYNAME="+entryName);
246             if (entry.isSymbolicLink())
247             {
248             log.info("###9 ENTRYNAME="+entryName);
249                 log.info("Creating symbolic link "+efile.getName()+" -> "+entry.getLinkName());
250                 Files.createSymbolicLink(efile.toPath(), Paths.get(entry.getLinkName()));
251             log.info("###10 ENTRYNAME="+entryName);
252             entry = tgz.getNextTarEntry();
253             log.info("###10a ENTRYNAME="+entryName);
254                 continue;
255             }
256             
257             log.info("###11 ENTRYNAME="+entryName);
258             try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
259                 InputStream tin = tgz;) {
260             log.info("###12 ENTRYNAME="+entryName);
261                 StreamUtil.copy(tin, fout);
262             } catch (Exception e) {
263             log.info("###13 ENTRYNAME="+entryName);
264                 throw new IOException(
265                     Log.format("Failure unpacking", "tgz", tgz, "entry", efile), e);
266             }
267             log.info("###14 ENTRYNAME="+entryName);
268             entry = tgz.getNextTarEntry();
269             log.info("###15 ENTRYNAME="+entryName);
270         }
271     }
272
273     /**
274      * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
275      * packedJar} has a {@code .gz} extension, it will be gunzipped first.
276      */
277     public static void unpackPacked200Jar (File packedJar, File target) throws IOException
278     {
279         try (InputStream packJarIn = new FileInputStream(packedJar);
280              JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
281             boolean gz = (packedJar.getName().endsWith(".gz") ||
282                           packedJar.getName().endsWith(".gz_new"));
283             try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
284                 Pack200.Unpacker unpacker = Pack200.newUnpacker();
285                 unpacker.unpack(packJarIn2, jarOut);
286             }
287         }
288     }
289
290     /**
291      * Copies the given {@code source} file to the given {@code target}.
292      */
293     public static void copy (File source, File target) throws IOException {
294         try (FileInputStream in = new FileInputStream(source);
295              FileOutputStream out = new FileOutputStream(target)) {
296             StreamUtil.copy(in, out);
297         }
298     }
299
300     /**
301      * Marks {@code file} as executable, if it exists. Catches and logs any errors that occur.
302      */
303     public static void makeExecutable (File file) {
304         try {
305             if (file.exists()) {
306                 if (!file.setExecutable(true, false)) {
307                     log.warning("Failed to mark as executable", "file", file);
308                 }
309             }
310         } catch (Exception e) {
311             log.warning("Failed to mark as executable", "file", file, "error", e);
312         }
313     }
314
315     /**
316      * Used by {@link #walkTree}.
317      */
318     public interface Visitor
319     {
320         void visit (File file);
321     }
322
323     /**
324      * Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
325      */
326     public static void walkTree (File root, Visitor visitor)
327     {
328         File[] children = root.listFiles();
329         if (children == null) return;
330         Deque<File> stack = new ArrayDeque<>(Arrays.asList(children));
331         while (!stack.isEmpty()) {
332             File currentFile = stack.pop();
333             if (currentFile.exists()) {
334                 visitor.visit(currentFile);
335                 File[] currentChildren = currentFile.listFiles();
336                 if (currentChildren != null) {
337                     for (File file : currentChildren) {
338                         stack.push(file);
339                     }
340                 }
341             }
342         }
343     }
344 }