1 /* 2 * Copyright (C) 2010, Google Inc. 3 * Copyright (C) 2010, Matthias Sohn <matthias.sohn@sap.com> 4 * Copyright (C) 2010, Jens Baumgart <jens.baumgart@sap.com> 5 * and other copyright owners as documented in the project's IP log. 6 * 7 * This program and the accompanying materials are made available 8 * under the terms of the Eclipse Distribution License v1.0 which 9 * accompanies this distribution, is reproduced below, and is 10 * available at http://www.eclipse.org/org/documents/edl-v10.php 11 * 12 * All rights reserved. 13 * 14 * Redistribution and use in source and binary forms, with or 15 * without modification, are permitted provided that the following 16 * conditions are met: 17 * 18 * - Redistributions of source code must retain the above copyright 19 * notice, this list of conditions and the following disclaimer. 20 * 21 * - Redistributions in binary form must reproduce the above 22 * copyright notice, this list of conditions and the following 23 * disclaimer in the documentation and/or other materials provided 24 * with the distribution. 25 * 26 * - Neither the name of the Eclipse Foundation, Inc. nor the 27 * names of its contributors may be used to endorse or promote 28 * products derived from this software without specific prior 29 * written permission. 30 * 31 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 32 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 33 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 34 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 35 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 36 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 37 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 38 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 39 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 40 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 41 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 42 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 43 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 44 */ 45 46 package org.eclipse.jgit.util; 47 48 import java.io.File; 49 import java.io.IOException; 50 import java.nio.channels.FileLock; 51 import java.text.MessageFormat; 52 import java.util.ArrayList; 53 import java.util.List; 54 import java.util.regex.Pattern; 55 56 import org.eclipse.jgit.internal.JGitText; 57 58 /** 59 * File Utilities 60 */ 61 public class FileUtils { 62 63 /** 64 * Option to delete given {@code File} 65 */ 66 public static final int NONE = 0; 67 68 /** 69 * Option to recursively delete given {@code File} 70 */ 71 public static final int RECURSIVE = 1; 72 73 /** 74 * Option to retry deletion if not successful 75 */ 76 public static final int RETRY = 2; 77 78 /** 79 * Option to skip deletion if file doesn't exist 80 */ 81 public static final int SKIP_MISSING = 4; 82 83 /** 84 * Option not to throw exceptions when a deletion finally doesn't succeed. 85 * @since 2.0 86 */ 87 public static final int IGNORE_ERRORS = 8; 88 89 /** 90 * Option to only delete empty directories. This option can be combined with 91 * {@link #RECURSIVE} 92 * 93 * @since 3.0 94 */ 95 public static final int EMPTY_DIRECTORIES_ONLY = 16; 96 97 /** 98 * Delete file or empty folder 99 * 100 * @param f 101 * {@code File} to be deleted 102 * @throws IOException 103 * if deletion of {@code f} fails. This may occur if {@code f} 104 * didn't exist when the method was called. This can therefore 105 * cause IOExceptions during race conditions when multiple 106 * concurrent threads all try to delete the same file. 107 */ 108 public static void delete(final File f) throws IOException { 109 delete(f, NONE); 110 } 111 112 /** 113 * Delete file or folder 114 * 115 * @param f 116 * {@code File} to be deleted 117 * @param options 118 * deletion options, {@code RECURSIVE} for recursive deletion of 119 * a subtree, {@code RETRY} to retry when deletion failed. 120 * Retrying may help if the underlying file system doesn't allow 121 * deletion of files being read by another thread. 122 * @throws IOException 123 * if deletion of {@code f} fails. This may occur if {@code f} 124 * didn't exist when the method was called. This can therefore 125 * cause IOExceptions during race conditions when multiple 126 * concurrent threads all try to delete the same file. This 127 * exception is not thrown when IGNORE_ERRORS is set. 128 */ 129 public static void delete(final File f, int options) throws IOException { 130 FS fs = FS.DETECTED; 131 if ((options & SKIP_MISSING) != 0 && !fs.exists(f)) 132 return; 133 134 if ((options & RECURSIVE) != 0 && fs.isDirectory(f)) { 135 final File[] items = f.listFiles(); 136 if (items != null) { 137 List<File> files = new ArrayList<File>(); 138 List<File> dirs = new ArrayList<File>(); 139 for (File c : items) 140 if (c.isFile()) 141 files.add(c); 142 else 143 dirs.add(c); 144 // Try to delete files first, otherwise options 145 // EMPTY_DIRECTORIES_ONLY|RECURSIVE will delete empty 146 // directories before aborting, depending on order. 147 for (File file : files) 148 delete(file, options); 149 for (File d : dirs) 150 delete(d, options); 151 } 152 } 153 154 boolean delete = false; 155 if ((options & EMPTY_DIRECTORIES_ONLY) != 0) { 156 if (f.isDirectory()) { 157 delete = true; 158 } else { 159 if ((options & IGNORE_ERRORS) == 0) 160 throw new IOException(MessageFormat.format( 161 JGitText.get().deleteFileFailed, 162 f.getAbsolutePath())); 163 } 164 } else { 165 delete = true; 166 } 167 168 if (delete && !f.delete()) { 169 if ((options & RETRY) != 0 && fs.exists(f)) { 170 for (int i = 1; i < 10; i++) { 171 try { 172 Thread.sleep(100); 173 } catch (InterruptedException e) { 174 // ignore 175 } 176 if (f.delete()) 177 return; 178 } 179 } 180 if ((options & IGNORE_ERRORS) == 0) 181 throw new IOException(MessageFormat.format( 182 JGitText.get().deleteFileFailed, f.getAbsolutePath())); 183 } 184 } 185 186 /** 187 * Rename a file or folder. If the rename fails and if we are running on a 188 * filesystem where it makes sense to repeat a failing rename then repeat 189 * the rename operation up to 9 times with 100ms sleep time between two 190 * calls. Furthermore if the destination exists and is directory hierarchy 191 * with only directories in it, the whole directory hierarchy will be 192 * deleted. If the target represents a non-empty directory structure, empty 193 * subdirectories within that structure may or may not be deleted even if 194 * the method fails. Furthermore if the destination exists and is a file 195 * then the file will be deleted and then the rename is retried. 196 * <p> 197 * This operation is <em>not</em> atomic. 198 * 199 * @see FS#retryFailedLockFileCommit() 200 * @param src 201 * the old {@code File} 202 * @param dst 203 * the new {@code File} 204 * @throws IOException 205 * if the rename has failed 206 * @since 3.0 207 */ 208 public static void rename(final File src, final File dst) 209 throws IOException { 210 int attempts = FS.DETECTED.retryFailedLockFileCommit() ? 10 : 1; 211 while (--attempts >= 0) { 212 if (src.renameTo(dst)) 213 return; 214 try { 215 if (!dst.delete()) 216 delete(dst, EMPTY_DIRECTORIES_ONLY | RECURSIVE); 217 // On *nix there is no try, you do or do not 218 if (src.renameTo(dst)) 219 return; 220 } catch (IOException e) { 221 // ignore and continue retry 222 } 223 try { 224 Thread.sleep(100); 225 } catch (InterruptedException e) { 226 throw new IOException(MessageFormat.format( 227 JGitText.get().renameFileFailed, src.getAbsolutePath(), 228 dst.getAbsolutePath())); 229 } 230 } 231 throw new IOException(MessageFormat.format( 232 JGitText.get().renameFileFailed, src.getAbsolutePath(), 233 dst.getAbsolutePath())); 234 } 235 236 /** 237 * Creates the directory named by this abstract pathname. 238 * 239 * @param d 240 * directory to be created 241 * @throws IOException 242 * if creation of {@code d} fails. This may occur if {@code d} 243 * did exist when the method was called. This can therefore 244 * cause IOExceptions during race conditions when multiple 245 * concurrent threads all try to create the same directory. 246 */ 247 public static void mkdir(final File d) 248 throws IOException { 249 mkdir(d, false); 250 } 251 252 /** 253 * Creates the directory named by this abstract pathname. 254 * 255 * @param d 256 * directory to be created 257 * @param skipExisting 258 * if {@code true} skip creation of the given directory if it 259 * already exists in the file system 260 * @throws IOException 261 * if creation of {@code d} fails. This may occur if {@code d} 262 * did exist when the method was called. This can therefore 263 * cause IOExceptions during race conditions when multiple 264 * concurrent threads all try to create the same directory. 265 */ 266 public static void mkdir(final File d, boolean skipExisting) 267 throws IOException { 268 if (!d.mkdir()) { 269 if (skipExisting && d.isDirectory()) 270 return; 271 throw new IOException(MessageFormat.format( 272 JGitText.get().mkDirFailed, d.getAbsolutePath())); 273 } 274 } 275 276 /** 277 * Creates the directory named by this abstract pathname, including any 278 * necessary but nonexistent parent directories. Note that if this operation 279 * fails it may have succeeded in creating some of the necessary parent 280 * directories. 281 * 282 * @param d 283 * directory to be created 284 * @throws IOException 285 * if creation of {@code d} fails. This may occur if {@code d} 286 * did exist when the method was called. This can therefore 287 * cause IOExceptions during race conditions when multiple 288 * concurrent threads all try to create the same directory. 289 */ 290 public static void mkdirs(final File d) throws IOException { 291 mkdirs(d, false); 292 } 293 294 /** 295 * Creates the directory named by this abstract pathname, including any 296 * necessary but nonexistent parent directories. Note that if this operation 297 * fails it may have succeeded in creating some of the necessary parent 298 * directories. 299 * 300 * @param d 301 * directory to be created 302 * @param skipExisting 303 * if {@code true} skip creation of the given directory if it 304 * already exists in the file system 305 * @throws IOException 306 * if creation of {@code d} fails. This may occur if {@code d} 307 * did exist when the method was called. This can therefore 308 * cause IOExceptions during race conditions when multiple 309 * concurrent threads all try to create the same directory. 310 */ 311 public static void mkdirs(final File d, boolean skipExisting) 312 throws IOException { 313 if (!d.mkdirs()) { 314 if (skipExisting && d.isDirectory()) 315 return; 316 throw new IOException(MessageFormat.format( 317 JGitText.get().mkDirsFailed, d.getAbsolutePath())); 318 } 319 } 320 321 /** 322 * Atomically creates a new, empty file named by this abstract pathname if 323 * and only if a file with this name does not yet exist. The check for the 324 * existence of the file and the creation of the file if it does not exist 325 * are a single operation that is atomic with respect to all other 326 * filesystem activities that might affect the file. 327 * <p> 328 * Note: this method should not be used for file-locking, as the resulting 329 * protocol cannot be made to work reliably. The {@link FileLock} facility 330 * should be used instead. 331 * 332 * @param f 333 * the file to be created 334 * @throws IOException 335 * if the named file already exists or if an I/O error occurred 336 */ 337 public static void createNewFile(File f) throws IOException { 338 if (!f.createNewFile()) 339 throw new IOException(MessageFormat.format( 340 JGitText.get().createNewFileFailed, f)); 341 } 342 343 /** 344 * Create a symbolic link 345 * 346 * @param path 347 * @param target 348 * @throws IOException 349 * @since 3.0 350 */ 351 public static void createSymLink(File path, String target) 352 throws IOException { 353 FS.DETECTED.createSymLink(path, target); 354 } 355 356 /** 357 * @param path 358 * @return the target of the symbolic link, or null if it is not a symbolic 359 * link 360 * @throws IOException 361 * @since 3.0 362 */ 363 public static String readSymLink(File path) throws IOException { 364 return FS.DETECTED.readSymLink(path); 365 } 366 367 /** 368 * Create a temporary directory. 369 * 370 * @param prefix 371 * @param suffix 372 * @param dir 373 * The parent dir, can be null to use system default temp dir. 374 * @return the temp dir created. 375 * @throws IOException 376 * @since 3.4 377 */ 378 public static File createTempDir(String prefix, String suffix, File dir) 379 throws IOException { 380 final int RETRIES = 1; // When something bad happens, retry once. 381 for (int i = 0; i < RETRIES; i++) { 382 File tmp = File.createTempFile(prefix, suffix, dir); 383 if (!tmp.delete()) 384 continue; 385 if (!tmp.mkdir()) 386 continue; 387 return tmp; 388 } 389 throw new IOException(JGitText.get().cannotCreateTempDir); 390 } 391 392 /** 393 * This will try and make a given path relative to another. 394 * <p> 395 * For example, if this is called with the two following paths : 396 * 397 * <pre> 398 * <code>base = "c:\\Users\\jdoe\\eclipse\\git\\project"</code> 399 * <code>other = "c:\\Users\\jdoe\\eclipse\\git\\another_project\\pom.xml"</code> 400 * </pre> 401 * 402 * This will return "..\\another_project\\pom.xml". 403 * </p> 404 * <p> 405 * This method uses {@link File#separator} to split the paths into segments. 406 * </p> 407 * <p> 408 * <b>Note</b> that this will return the empty String if <code>base</code> 409 * and <code>other</code> are equal. 410 * </p> 411 * 412 * @param base 413 * The path against which <code>other</code> should be 414 * relativized. This will be assumed to denote the path to a 415 * folder and not a file. 416 * @param other 417 * The path that will be made relative to <code>base</code>. 418 * @return A relative path that, when resolved against <code>base</code>, 419 * will yield the original <code>other</code>. 420 * @since 3.7 421 */ 422 public static String relativize(String base, String other) { 423 if (base.equals(other)) 424 return ""; //$NON-NLS-1$ 425 426 final boolean ignoreCase = !FS.DETECTED.isCaseSensitive(); 427 final String[] baseSegments = base.split(Pattern.quote(File.separator)); 428 final String[] otherSegments = other.split(Pattern 429 .quote(File.separator)); 430 431 int commonPrefix = 0; 432 while (commonPrefix < baseSegments.length 433 && commonPrefix < otherSegments.length) { 434 if (ignoreCase 435 && baseSegments[commonPrefix] 436 .equalsIgnoreCase(otherSegments[commonPrefix])) 437 commonPrefix++; 438 else if (!ignoreCase 439 && baseSegments[commonPrefix] 440 .equals(otherSegments[commonPrefix])) 441 commonPrefix++; 442 else 443 break; 444 } 445 446 final StringBuilder builder = new StringBuilder(); 447 for (int i = commonPrefix; i < baseSegments.length; i++) 448 builder.append("..").append(File.separator); //$NON-NLS-1$ 449 for (int i = commonPrefix; i < otherSegments.length; i++) { 450 builder.append(otherSegments[i]); 451 if (i < otherSegments.length - 1) 452 builder.append(File.separator); 453 } 454 return builder.toString(); 455 } 456 }