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 = tgz.getNextTarEntry();
185 while (entry != null) {
186 // sanitize the entry name
187 String entryName = entry.getName();
188 log.info("### ENTRYNAME="+entryName);
189 if (entryName.startsWith(File.separator))
191 entryName = entryName.substring(File.separator.length());
193 File efile = new File(target, entryName);
194 log.info("###1 ENTRYNAME="+entryName);
196 // if we're unpacking a normal tgz file, it will have special path
197 // entries that allow us to create our directories first
198 if (entry.isDirectory()) {
199 log.info("###2 ENTRYNAME="+entryName);
200 log.info("Directory '"+entryName+"', cleanExistingDirs="+cleanExistingDirs);
201 if (cleanExistingDirs) {
202 if (efile.exists()) {
203 log.info("###3 ENTRYNAME="+entryName);
204 for (File f : efile.listFiles()) {
205 log.info("###4 ENTRYNAME="+entryName);
206 if (!f.isDirectory()) {
207 log.info("Attempting to delete "+f.getName());
214 log.info("###5 ENTRYNAME="+entryName);
215 if (!efile.exists() && !efile.mkdir()) {
216 log.warning("Failed to create tgz entry path", "tgz", tgz, "entry", entry);
218 entry = tgz.getNextTarEntry();
219 log.info("###5a ENTRYNAME="+entryName);
223 // but some do not, so we want to ensure that our directories exist
224 // prior to getting down and funky
225 File parent = new File(efile.getParent());
226 if (!parent.exists() && !parent.mkdirs()) {
227 log.warning("Failed to create tgz entry parent", "tgz", tgz, "parent", parent);
228 entry = tgz.getNextTarEntry();
229 log.info("###7a ENTRYNAME="+entryName);
233 log.info("###6 ENTRYNAME="+entryName);
236 log.info("###7 ENTRYNAME="+entryName);
237 log.info("Creating hard link "+efile.getName()+" -> "+entry.getLinkName());
238 Files.createLink(efile.toPath(), Paths.get(entry.getLinkName()));
239 log.info("###7a ENTRYNAME="+entryName);
240 entry = tgz.getNextTarEntry();
241 log.info("###7a ENTRYNAME="+entryName);
245 log.info("###8 ENTRYNAME="+entryName);
246 if (entry.isSymbolicLink())
248 log.info("###9 ENTRYNAME="+entryName);
249 log.info("Creating symbolic link "+efile.getName()+" -> "+entry.getLinkName());
250 Files.createSymbolicLink(efile.toPath(), Paths.get(entry.getLinkName()));
251 log.info("###10 ENTRYNAME="+entryName);
252 entry = tgz.getNextTarEntry();
253 log.info("###10a ENTRYNAME="+entryName);
257 log.info("###11 ENTRYNAME="+entryName);
258 try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
259 InputStream tin = tgz;) {
260 log.info("###12 ENTRYNAME="+entryName);
261 StreamUtil.copy(tin, fout);
262 } catch (Exception e) {
263 log.info("###13 ENTRYNAME="+entryName);
264 throw new IOException(
265 Log.format("Failure unpacking", "tgz", tgz, "entry", efile), e);
267 log.info("###14 ENTRYNAME="+entryName);
268 entry = tgz.getNextTarEntry();
269 log.info("###15 ENTRYNAME="+entryName);
274 * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
275 * packedJar} has a {@code .gz} extension, it will be gunzipped first.
277 public static void unpackPacked200Jar (File packedJar, File target) throws IOException
279 try (InputStream packJarIn = new FileInputStream(packedJar);
280 JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
281 boolean gz = (packedJar.getName().endsWith(".gz") ||
282 packedJar.getName().endsWith(".gz_new"));
283 try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
284 Pack200.Unpacker unpacker = Pack200.newUnpacker();
285 unpacker.unpack(packJarIn2, jarOut);
291 * Copies the given {@code source} file to the given {@code target}.
293 public static void copy (File source, File target) throws IOException {
294 try (FileInputStream in = new FileInputStream(source);
295 FileOutputStream out = new FileOutputStream(target)) {
296 StreamUtil.copy(in, out);
301 * Marks {@code file} as executable, if it exists. Catches and logs any errors that occur.
303 public static void makeExecutable (File file) {
306 if (!file.setExecutable(true, false)) {
307 log.warning("Failed to mark as executable", "file", file);
310 } catch (Exception e) {
311 log.warning("Failed to mark as executable", "file", file, "error", e);
316 * Used by {@link #walkTree}.
318 public interface Visitor
320 void visit (File file);
324 * Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
326 public static void walkTree (File root, Visitor visitor)
328 File[] children = root.listFiles();
329 if (children == null) return;
330 Deque<File> stack = new ArrayDeque<>(Arrays.asList(children));
331 while (!stack.isEmpty()) {
332 File currentFile = stack.pop();
333 if (currentFile.exists()) {
334 visitor.visit(currentFile);
335 File[] currentChildren = currentFile.listFiles();
336 if (currentChildren != null) {
337 for (File file : currentChildren) {