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