JAL-3394 A little streamlining and fixing of helper script
[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;
185                 while ((entry = tgz.getNextTarEntry()) != null) {
186             // sanitize the entry name
187                         String entryName = entry.getName();
188                         if (entryName.startsWith(File.separator))
189                         {
190                                 entryName = entryName.substring(File.separator.length());
191                         }
192             File efile = new File(target, entryName);
193
194             // if we're unpacking a normal tgz file, it will have special path
195             // entries that allow us to create our directories first
196             if (entry.isDirectory()) {
197                 
198                                 if (cleanExistingDirs) {
199                     if (efile.exists()) {
200                         for (File f : efile.listFiles()) {
201                             if (!f.isDirectory())
202                             f.delete();
203                         }
204                     }
205                                 }
206                                 
207                 if (!efile.exists() && !efile.mkdir()) {
208                     log.warning("Failed to create tgz entry path", "tgz", tgz, "entry", entry);
209                 }
210                 continue;
211             }
212
213             // but some do not, so we want to ensure that our directories exist
214             // prior to getting down and funky
215             File parent = new File(efile.getParent());
216             if (!parent.exists() && !parent.mkdirs()) {
217                 log.warning("Failed to create tgz entry parent", "tgz", tgz, "parent", parent);
218                 continue;
219             }
220
221             if (entry.isLink())
222             {
223                 System.out.println("Creating hard link "+efile.getName()+" -> "+entry.getLinkName());
224                 Files.createLink(efile.toPath(), Paths.get(entry.getLinkName()));
225                 continue;
226             }
227
228             if (entry.isSymbolicLink())
229             {
230                 System.out.println("Creating symbolic link "+efile.getName()+" -> "+entry.getLinkName());
231                 Files.createSymbolicLink(efile.toPath(), Paths.get(entry.getLinkName()));
232                 continue;
233             }
234             
235             try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
236                 InputStream tin = tgz;) {
237                 StreamUtil.copy(tin, fout);
238             } catch (Exception e) {
239                 throw new IOException(
240                     Log.format("Failure unpacking", "tgz", tgz, "entry", efile), e);
241             }
242         }
243     }
244
245     /**
246      * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
247      * packedJar} has a {@code .gz} extension, it will be gunzipped first.
248      */
249     public static void unpackPacked200Jar (File packedJar, File target) throws IOException
250     {
251         try (InputStream packJarIn = new FileInputStream(packedJar);
252              JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
253             boolean gz = (packedJar.getName().endsWith(".gz") ||
254                           packedJar.getName().endsWith(".gz_new"));
255             try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
256                 Pack200.Unpacker unpacker = Pack200.newUnpacker();
257                 unpacker.unpack(packJarIn2, jarOut);
258             }
259         }
260     }
261
262     /**
263      * Copies the given {@code source} file to the given {@code target}.
264      */
265     public static void copy (File source, File target) throws IOException {
266         try (FileInputStream in = new FileInputStream(source);
267              FileOutputStream out = new FileOutputStream(target)) {
268             StreamUtil.copy(in, out);
269         }
270     }
271
272     /**
273      * Marks {@code file} as executable, if it exists. Catches and logs any errors that occur.
274      */
275     public static void makeExecutable (File file) {
276         try {
277             if (file.exists()) {
278                 if (!file.setExecutable(true, false)) {
279                     log.warning("Failed to mark as executable", "file", file);
280                 }
281             }
282         } catch (Exception e) {
283             log.warning("Failed to mark as executable", "file", file, "error", e);
284         }
285     }
286
287     /**
288      * Used by {@link #walkTree}.
289      */
290     public interface Visitor
291     {
292         void visit (File file);
293     }
294
295     /**
296      * Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
297      */
298     public static void walkTree (File root, Visitor visitor)
299     {
300         File[] children = root.listFiles();
301         if (children == null) return;
302         Deque<File> stack = new ArrayDeque<>(Arrays.asList(children));
303         while (!stack.isEmpty()) {
304             File currentFile = stack.pop();
305             if (currentFile.exists()) {
306                 visitor.visit(currentFile);
307                 File[] currentChildren = currentFile.listFiles();
308                 if (currentChildren != null) {
309                     for (File file : currentChildren) {
310                         stack.push(file);
311                     }
312                 }
313             }
314         }
315     }
316 }