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 / 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.util.*;
10 import java.util.jar.*;
11 import java.util.zip.GZIPInputStream;
12
13 import com.threerings.getdown.Log;
14 import static com.threerings.getdown.Log.log;
15
16 /**
17  * File related utilities.
18  */
19 public class FileUtil
20 {
21     /**
22      * Gets the specified source file to the specified destination file by hook or crook. Windows
23      * has all sorts of problems which we work around in this method.
24      *
25      * @return true if we managed to get the job done, false otherwise.
26      */
27     public static boolean renameTo (File source, File dest)
28     {
29         // if we're on a civilized operating system we may be able to simple rename it
30         if (source.renameTo(dest)) {
31             return true;
32         }
33
34         // fall back to trying to rename the old file out of the way, rename the new file into
35         // place and then delete the old file
36         if (dest.exists()) {
37             File temp = new File(dest.getPath() + "_old");
38             if (temp.exists() && !deleteHarder(temp)) {
39                 log.warning("Failed to delete old intermediate file " + temp + ".");
40                 // the subsequent code will probably fail
41             }
42             if (dest.renameTo(temp) && source.renameTo(dest)) {
43                 if (!deleteHarder(temp)) {
44                     log.warning("Failed to delete intermediate file " + temp + ".");
45                 }
46                 return true;
47             }
48         }
49
50         // as a last resort, try copying the old data over the new
51         try {
52             copy(source, dest);
53         } catch (IOException ioe) {
54             log.warning("Failed to copy " + source + " to " + dest + ": " + ioe);
55             return false;
56         }
57
58         if (!deleteHarder(source)) {
59             log.warning("Failed to delete " + source + " after brute force copy to " + dest + ".");
60         }
61         return true;
62     }
63
64     /**
65      * "Tries harder" to delete {@code file} than just calling {@code delete} on it. Presently this
66      * just means "try a second time if the first time fails, and if that fails then try to delete
67      * when the virtual machine terminates." On Windows Vista, sometimes deletes fail but then
68      * succeed if you just try again. Given that delete failure is a rare occurrence, we can
69      * implement this hacky workaround without any negative consequences for normal behavior.
70      */
71     public static boolean deleteHarder (File file) {
72         // if at first you don't succeed... try, try again
73         boolean deleted = (file.delete() || file.delete());
74         if (!deleted) {
75             file.deleteOnExit();
76         }
77         return deleted;
78     }
79
80     /**
81      * Force deletes {@code file} and all of its children recursively using {@link #deleteHarder}.
82      * Note that some children may still be deleted even if {@code false} is returned. Also, since
83      * {@link #deleteHarder} is used, the {@code file} could be deleted once the jvm exits even if
84      * {@code false} is returned.
85      *
86      * @param file file to delete.
87      * @return true iff {@code file} was successfully deleted.
88      */
89     public static boolean deleteDirHarder (File file) {
90         if (file.isDirectory()) {
91             for (File child : file.listFiles()) {
92                 deleteDirHarder(child);
93             }
94         }
95         return deleteHarder(file);
96     }
97
98     /**
99      * Reads the contents of the supplied input stream into a list of lines. Closes the reader on
100      * successful or failed completion.
101      */
102     public static List<String> readLines (Reader in)
103         throws IOException
104     {
105         List<String> lines = new ArrayList<>();
106         try (BufferedReader bin = new BufferedReader(in)) {
107             for (String line = null; (line = bin.readLine()) != null; lines.add(line)) {}
108         }
109         return lines;
110     }
111
112     /**
113      * Unpacks the specified jar file into the specified target directory.
114      * @param cleanExistingDirs if true, all files in all directories contained in {@code jar} will
115      * be deleted prior to unpacking the jar.
116      */
117     public static void unpackJar (JarFile jar, File target, boolean cleanExistingDirs)
118         throws IOException
119     {
120         if (cleanExistingDirs) {
121             Enumeration<?> entries = jar.entries();
122             while (entries.hasMoreElements()) {
123                 JarEntry entry = (JarEntry)entries.nextElement();
124                 if (entry.isDirectory()) {
125                     File efile = new File(target, entry.getName());
126                     if (efile.exists()) {
127                         for (File f : efile.listFiles()) {
128                             if (!f.isDirectory())
129                             f.delete();
130                         }
131                     }
132                 }
133             }
134         }
135
136         Enumeration<?> entries = jar.entries();
137         while (entries.hasMoreElements()) {
138             JarEntry entry = (JarEntry)entries.nextElement();
139             File efile = new File(target, entry.getName());
140
141             // if we're unpacking a normal jar file, it will have special path
142             // entries that allow us to create our directories first
143             if (entry.isDirectory()) {
144                 if (!efile.exists() && !efile.mkdir()) {
145                     log.warning("Failed to create jar entry path", "jar", jar, "entry", entry);
146                 }
147                 continue;
148             }
149
150             // but some do not, so we want to ensure that our directories exist
151             // prior to getting down and funky
152             File parent = new File(efile.getParent());
153             if (!parent.exists() && !parent.mkdirs()) {
154                 log.warning("Failed to create jar entry parent", "jar", jar, "parent", parent);
155                 continue;
156             }
157
158             try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
159                  InputStream jin = jar.getInputStream(entry)) {
160                 StreamUtil.copy(jin, fout);
161             } catch (Exception e) {
162                 throw new IOException(
163                     Log.format("Failure unpacking", "jar", jar, "entry", efile), e);
164             }
165         }
166     }
167
168     /**
169      * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
170      * packedJar} has a {@code .gz} extension, it will be gunzipped first.
171      */
172     public static void unpackPacked200Jar (File packedJar, File target) throws IOException
173     {
174         try (InputStream packJarIn = new FileInputStream(packedJar);
175              JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
176             boolean gz = (packedJar.getName().endsWith(".gz") ||
177                           packedJar.getName().endsWith(".gz_new"));
178             try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
179                 Pack200.Unpacker unpacker = Pack200.newUnpacker();
180                 unpacker.unpack(packJarIn2, jarOut);
181             }
182         }
183     }
184
185     /**
186      * Copies the given {@code source} file to the given {@code target}.
187      */
188     public static void copy (File source, File target) throws IOException {
189         try (FileInputStream in = new FileInputStream(source);
190              FileOutputStream out = new FileOutputStream(target)) {
191             StreamUtil.copy(in, out);
192         }
193     }
194
195     /**
196      * Marks {@code file} as executable, if it exists. Catches and logs any errors that occur.
197      */
198     public static void makeExecutable (File file) {
199         try {
200             if (file.exists()) {
201                 if (!file.setExecutable(true, false)) {
202                     log.warning("Failed to mark as executable", "file", file);
203                 }
204             }
205         } catch (Exception e) {
206             log.warning("Failed to mark as executable", "file", file, "error", e);
207         }
208     }
209
210     /**
211      * Used by {@link #walkTree}.
212      */
213     public interface Visitor
214     {
215         void visit (File file);
216     }
217
218     /**
219      * Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
220      */
221     public static void walkTree (File root, Visitor visitor)
222     {
223         File[] children = root.listFiles();
224         if (children == null) return;
225         Deque<File> stack = new ArrayDeque<>(Arrays.asList(children));
226         while (!stack.isEmpty()) {
227             File currentFile = stack.pop();
228             if (currentFile.exists()) {
229                 visitor.visit(currentFile);
230                 File[] currentChildren = currentFile.listFiles();
231                 if (currentChildren != null) {
232                     for (File file : currentChildren) {
233                         stack.push(file);
234                     }
235                 }
236             }
237         }
238     }
239 }