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 */ renameTo(File source, File dest)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 */ deleteHarder(File file)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 */ deleteDirHarder(File file)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 */ readLines(Reader in)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 */ unpackJar(JarFile jar, File target, boolean cleanExistingDirs)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 */ unpackPacked200Jar(File packedJar, File target)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 */ copy(File source, File target)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 */ makeExecutable(File file)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 { visit(File file)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 */ walkTree(File root, Visitor visitor)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 } 243