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
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java
new file mode 100644 (file)
index 0000000..ec9d887
--- /dev/null
@@ -0,0 +1,239 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.util;
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+import java.util.zip.GZIPInputStream;
+
+import com.threerings.getdown.Log;
+import static com.threerings.getdown.Log.log;
+
+/**
+ * File related utilities.
+ */
+public class FileUtil
+{
+    /**
+     * Gets the specified source file to the specified destination file by hook or crook. Windows
+     * has all sorts of problems which we work around in this method.
+     *
+     * @return true if we managed to get the job done, false otherwise.
+     */
+    public static boolean renameTo (File source, File dest)
+    {
+        // if we're on a civilized operating system we may be able to simple rename it
+        if (source.renameTo(dest)) {
+            return true;
+        }
+
+        // fall back to trying to rename the old file out of the way, rename the new file into
+        // place and then delete the old file
+        if (dest.exists()) {
+            File temp = new File(dest.getPath() + "_old");
+            if (temp.exists() && !deleteHarder(temp)) {
+                log.warning("Failed to delete old intermediate file " + temp + ".");
+                // the subsequent code will probably fail
+            }
+            if (dest.renameTo(temp) && source.renameTo(dest)) {
+                if (!deleteHarder(temp)) {
+                    log.warning("Failed to delete intermediate file " + temp + ".");
+                }
+                return true;
+            }
+        }
+
+        // as a last resort, try copying the old data over the new
+        try {
+            copy(source, dest);
+        } catch (IOException ioe) {
+            log.warning("Failed to copy " + source + " to " + dest + ": " + ioe);
+            return false;
+        }
+
+        if (!deleteHarder(source)) {
+            log.warning("Failed to delete " + source + " after brute force copy to " + dest + ".");
+        }
+        return true;
+    }
+
+    /**
+     * "Tries harder" to delete {@code file} than just calling {@code delete} on it. Presently this
+     * just means "try a second time if the first time fails, and if that fails then try to delete
+     * when the virtual machine terminates." On Windows Vista, sometimes deletes fail but then
+     * succeed if you just try again. Given that delete failure is a rare occurrence, we can
+     * implement this hacky workaround without any negative consequences for normal behavior.
+     */
+    public static boolean deleteHarder (File file) {
+        // if at first you don't succeed... try, try again
+        boolean deleted = (file.delete() || file.delete());
+        if (!deleted) {
+            file.deleteOnExit();
+        }
+        return deleted;
+    }
+
+    /**
+     * Force deletes {@code file} and all of its children recursively using {@link #deleteHarder}.
+     * Note that some children may still be deleted even if {@code false} is returned. Also, since
+     * {@link #deleteHarder} is used, the {@code file} could be deleted once the jvm exits even if
+     * {@code false} is returned.
+     *
+     * @param file file to delete.
+     * @return true iff {@code file} was successfully deleted.
+     */
+    public static boolean deleteDirHarder (File file) {
+        if (file.isDirectory()) {
+            for (File child : file.listFiles()) {
+                deleteDirHarder(child);
+            }
+        }
+        return deleteHarder(file);
+    }
+
+    /**
+     * Reads the contents of the supplied input stream into a list of lines. Closes the reader on
+     * successful or failed completion.
+     */
+    public static List<String> readLines (Reader in)
+        throws IOException
+    {
+        List<String> lines = new ArrayList<>();
+        try (BufferedReader bin = new BufferedReader(in)) {
+            for (String line = null; (line = bin.readLine()) != null; lines.add(line)) {}
+        }
+        return lines;
+    }
+
+    /**
+     * Unpacks the specified jar file into the specified target directory.
+     * @param cleanExistingDirs if true, all files in all directories contained in {@code jar} will
+     * be deleted prior to unpacking the jar.
+     */
+    public static void unpackJar (JarFile jar, File target, boolean cleanExistingDirs)
+        throws IOException
+    {
+        if (cleanExistingDirs) {
+            Enumeration<?> entries = jar.entries();
+            while (entries.hasMoreElements()) {
+                JarEntry entry = (JarEntry)entries.nextElement();
+                if (entry.isDirectory()) {
+                    File efile = new File(target, entry.getName());
+                    if (efile.exists()) {
+                        for (File f : efile.listFiles()) {
+                            if (!f.isDirectory())
+                            f.delete();
+                        }
+                    }
+                }
+            }
+        }
+
+        Enumeration<?> entries = jar.entries();
+        while (entries.hasMoreElements()) {
+            JarEntry entry = (JarEntry)entries.nextElement();
+            File efile = new File(target, entry.getName());
+
+            // if we're unpacking a normal jar file, it will have special path
+            // entries that allow us to create our directories first
+            if (entry.isDirectory()) {
+                if (!efile.exists() && !efile.mkdir()) {
+                    log.warning("Failed to create jar entry path", "jar", jar, "entry", entry);
+                }
+                continue;
+            }
+
+            // but some do not, so we want to ensure that our directories exist
+            // prior to getting down and funky
+            File parent = new File(efile.getParent());
+            if (!parent.exists() && !parent.mkdirs()) {
+                log.warning("Failed to create jar entry parent", "jar", jar, "parent", parent);
+                continue;
+            }
+
+            try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
+                 InputStream jin = jar.getInputStream(entry)) {
+                StreamUtil.copy(jin, fout);
+            } catch (Exception e) {
+                throw new IOException(
+                    Log.format("Failure unpacking", "jar", jar, "entry", efile), e);
+            }
+        }
+    }
+
+    /**
+     * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
+     * packedJar} has a {@code .gz} extension, it will be gunzipped first.
+     */
+    public static void unpackPacked200Jar (File packedJar, File target) throws IOException
+    {
+        try (InputStream packJarIn = new FileInputStream(packedJar);
+             JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
+            boolean gz = (packedJar.getName().endsWith(".gz") ||
+                          packedJar.getName().endsWith(".gz_new"));
+            try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
+                Pack200.Unpacker unpacker = Pack200.newUnpacker();
+                unpacker.unpack(packJarIn2, jarOut);
+            }
+        }
+    }
+
+    /**
+     * Copies the given {@code source} file to the given {@code target}.
+     */
+    public static void copy (File source, File target) throws IOException {
+        try (FileInputStream in = new FileInputStream(source);
+             FileOutputStream out = new FileOutputStream(target)) {
+            StreamUtil.copy(in, out);
+        }
+    }
+
+    /**
+     * Marks {@code file} as executable, if it exists. Catches and logs any errors that occur.
+     */
+    public static void makeExecutable (File file) {
+        try {
+            if (file.exists()) {
+                if (!file.setExecutable(true, false)) {
+                    log.warning("Failed to mark as executable", "file", file);
+                }
+            }
+        } catch (Exception e) {
+            log.warning("Failed to mark as executable", "file", file, "error", e);
+        }
+    }
+
+    /**
+     * Used by {@link #walkTree}.
+     */
+    public interface Visitor
+    {
+        void visit (File file);
+    }
+
+    /**
+     * Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
+     */
+    public static void walkTree (File root, Visitor visitor)
+    {
+        File[] children = root.listFiles();
+        if (children == null) return;
+        Deque<File> stack = new ArrayDeque<>(Arrays.asList(children));
+        while (!stack.isEmpty()) {
+            File currentFile = stack.pop();
+            if (currentFile.exists()) {
+                visitor.visit(currentFile);
+                File[] currentChildren = currentFile.listFiles();
+                if (currentChildren != null) {
+                    for (File file : currentChildren) {
+                        stack.push(file);
+                    }
+                }
+            }
+        }
+    }
+}