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 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;
20 import com.threerings.getdown.util.StreamUtil;
21 import com.threerings.getdown.Log;
22 import static com.threerings.getdown.Log.log;
25 * File related utilities.
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.
33 * @return true if we managed to get the job done, false otherwise.
35 public static boolean renameTo (File source, File dest)
37 // if we're on a civilized operating system we may be able to simple rename it
38 if (source.renameTo(dest)) {
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
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
50 if (dest.renameTo(temp) && source.renameTo(dest)) {
51 if (!deleteHarder(temp)) {
52 log.warning("Failed to delete intermediate file " + temp + ".");
58 // as a last resort, try copying the old data over the new
61 } catch (IOException ioe) {
62 log.warning("Failed to copy " + source + " to " + dest + ": " + ioe);
66 if (!deleteHarder(source)) {
67 log.warning("Failed to delete " + source + " after brute force copy to " + dest + ".");
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.
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());
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.
94 * @param file file to delete.
95 * @return true iff {@code file} was successfully deleted.
97 public static boolean deleteDirHarder (File file) {
98 if (file.isDirectory()) {
99 for (File child : file.listFiles()) {
100 deleteDirHarder(child);
103 return deleteHarder(file);
107 * Reads the contents of the supplied input stream into a list of lines. Closes the reader on
108 * successful or failed completion.
110 public static List<String> readLines (Reader in)
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)) {}
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.
125 public static void unpackJar (JarFile jar, File target, boolean cleanExistingDirs)
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())
144 Enumeration<?> entries = jar.entries();
145 while (entries.hasMoreElements()) {
146 JarEntry entry = (JarEntry)entries.nextElement();
147 File efile = new File(target, entry.getName());
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);
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);
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);
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.
181 public static void unpackTgz (TarArchiveInputStream tgz, File target, boolean cleanExistingDirs)
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))
190 entryName = entryName.substring(File.separator.length());
192 File efile = new File(target, entryName);
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()) {
198 if (cleanExistingDirs) {
199 if (efile.exists()) {
200 for (File f : efile.listFiles()) {
201 if (!f.isDirectory())
207 if (!efile.exists() && !efile.mkdir()) {
208 log.warning("Failed to create tgz entry path", "tgz", tgz, "entry", entry);
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);
223 System.out.println("Creating hard link "+efile.getName()+" -> "+entry.getLinkName());
224 Files.createLink(efile.toPath(), Paths.get(entry.getLinkName()));
228 if (entry.isSymbolicLink())
230 System.out.println("Creating symbolic link "+efile.getName()+" -> "+entry.getLinkName());
231 Files.createSymbolicLink(efile.toPath(), Paths.get(entry.getLinkName()));
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);
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.
249 public static void unpackPacked200Jar (File packedJar, File target) throws IOException
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);
263 * Copies the given {@code source} file to the given {@code target}.
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);
273 * Marks {@code file} as executable, if it exists. Catches and logs any errors that occur.
275 public static void makeExecutable (File file) {
278 if (!file.setExecutable(true, false)) {
279 log.warning("Failed to mark as executable", "file", file);
282 } catch (Exception e) {
283 log.warning("Failed to mark as executable", "file", file, "error", e);
288 * Used by {@link #walkTree}.
290 public interface Visitor
292 void visit (File file);
296 * Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
298 public static void walkTree (File root, Visitor visitor)
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) {