2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
6 package com.threerings.getdown.util;
9 import java.nio.file.Files;
10 import java.nio.file.Paths;
12 import java.util.jar.*;
13 import java.util.zip.GZIPInputStream;
15 import com.threerings.getdown.util.StreamUtil;
16 import com.threerings.getdown.Log;
17 import static com.threerings.getdown.Log.log;
20 * File related utilities.
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.
28 * @return true if we managed to get the job done, false otherwise.
30 public static boolean renameTo (File source, File dest)
32 // if we're on a civilized operating system we may be able to simple rename it
33 if (source.renameTo(dest)) {
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
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
45 if (dest.renameTo(temp) && source.renameTo(dest)) {
46 if (!deleteHarder(temp)) {
47 log.warning("Failed to delete intermediate file " + temp + ".");
53 // as a last resort, try copying the old data over the new
56 } catch (IOException ioe) {
57 log.warning("Failed to copy " + source + " to " + dest + ": " + ioe);
61 if (!deleteHarder(source)) {
62 log.warning("Failed to delete " + source + " after brute force copy to " + dest + ".");
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.
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());
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.
89 * @param file file to delete.
90 * @return true iff {@code file} was successfully deleted.
92 public static boolean deleteDirHarder (File file) {
93 if (file.isDirectory()) {
94 for (File child : file.listFiles()) {
95 deleteDirHarder(child);
98 return deleteHarder(file);
102 * Reads the contents of the supplied input stream into a list of lines. Closes the reader on
103 * successful or failed completion.
105 public static List<String> readLines (Reader in)
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)) {}
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.
120 public static void unpackJar (JarFile jar, File target, boolean cleanExistingDirs)
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())
139 Enumeration<?> entries = jar.entries();
140 while (entries.hasMoreElements()) {
141 JarEntry entry = (JarEntry)entries.nextElement();
142 File efile = new File(target, entry.getName());
143 if (!efile.toPath().normalize().startsWith(target.toPath().normalize())) {
144 throw new IOException("Bad zip entry");
147 // if we're unpacking a normal jar file, it will have special path
148 // entries that allow us to create our directories first
149 if (entry.isDirectory()) {
150 if (!efile.exists() && !efile.mkdir()) {
151 log.warning("Failed to create jar entry path", "jar", jar, "entry", entry);
156 // but some do not, so we want to ensure that our directories exist
157 // prior to getting down and funky
158 File parent = new File(efile.getParent());
159 if (!parent.exists() && !parent.mkdirs()) {
160 log.warning("Failed to create jar entry parent", "jar", jar, "parent", parent);
164 try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
165 InputStream jin = jar.getInputStream(entry)) {
166 StreamUtil.copy(jin, fout);
167 } catch (Exception e) {
168 throw new IOException(
169 Log.format("Failure unpacking", "jar", jar, "entry", efile), e);
175 * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
176 * packedJar} has a {@code .gz} extension, it will be gunzipped first.
178 public static void unpackPacked200Jar (File packedJar, File target) throws IOException
180 try (InputStream packJarIn = new FileInputStream(packedJar);
181 JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
182 boolean gz = (packedJar.getName().endsWith(".gz") ||
183 packedJar.getName().endsWith(".gz_new"));
184 try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
185 Pack200.Unpacker unpacker = Pack200.newUnpacker();
186 unpacker.unpack(packJarIn2, jarOut);
192 * Copies the given {@code source} file to the given {@code target}.
194 public static void copy (File source, File target) throws IOException {
195 try (FileInputStream in = new FileInputStream(source);
196 FileOutputStream out = new FileOutputStream(target)) {
197 StreamUtil.copy(in, out);
202 * Marks {@code file} as executable, if it exists. Catches and logs any errors that occur.
204 public static void makeExecutable (File file) {
207 if (!file.setExecutable(true, false)) {
208 log.warning("Failed to mark as executable", "file", file);
211 } catch (Exception e) {
212 log.warning("Failed to mark as executable", "file", file, "error", e);
217 * Used by {@link #walkTree}.
219 public interface Visitor
221 void visit (File file);
225 * Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
227 public static void walkTree (File root, Visitor visitor)
229 File[] children = root.listFiles();
230 if (children == null) return;
231 Deque<File> stack = new ArrayDeque<>(Arrays.asList(children));
232 while (!stack.isEmpty()) {
233 File currentFile = stack.pop();
234 if (currentFile.exists()) {
235 visitor.visit(currentFile);
236 File[] currentChildren = currentFile.listFiles();
237 if (currentChildren != null) {
238 for (File file : currentChildren) {