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 static java.nio.charset.StandardCharsets.UTF_8; 49 50 import java.io.File; 51 import java.io.IOException; 52 import java.nio.file.AtomicMoveNotSupportedException; 53 import java.nio.file.CopyOption; 54 import java.nio.file.Files; 55 import java.nio.file.InvalidPathException; 56 import java.nio.file.LinkOption; 57 import java.nio.file.Path; 58 import java.nio.file.StandardCopyOption; 59 import java.nio.file.attribute.BasicFileAttributeView; 60 import java.nio.file.attribute.BasicFileAttributes; 61 import java.nio.file.attribute.FileTime; 62 import java.nio.file.attribute.PosixFileAttributeView; 63 import java.nio.file.attribute.PosixFileAttributes; 64 import java.nio.file.attribute.PosixFilePermission; 65 import java.text.MessageFormat; 66 import java.text.Normalizer; 67 import java.text.Normalizer.Form; 68 import java.util.ArrayList; 69 import java.util.List; 70 import java.util.Locale; 71 import java.util.regex.Pattern; 72 73 import org.eclipse.jgit.internal.JGitText; 74 import org.eclipse.jgit.lib.Constants; 75 import org.eclipse.jgit.util.FS.Attributes; 76 77 /** 78 * File Utilities 79 */ 80 public class FileUtils { 81 82 /** 83 * Option to delete given {@code File} 84 */ 85 public static final int NONE = 0; 86 87 /** 88 * Option to recursively delete given {@code File} 89 */ 90 public static final int RECURSIVE = 1; 91 92 /** 93 * Option to retry deletion if not successful 94 */ 95 public static final int RETRY = 2; 96 97 /** 98 * Option to skip deletion if file doesn't exist 99 */ 100 public static final int SKIP_MISSING = 4; 101 102 /** 103 * Option not to throw exceptions when a deletion finally doesn't succeed. 104 * @since 2.0 105 */ 106 public static final int IGNORE_ERRORS = 8; 107 108 /** 109 * Option to only delete empty directories. This option can be combined with 110 * {@link #RECURSIVE} 111 * 112 * @since 3.0 113 */ 114 public static final int EMPTY_DIRECTORIES_ONLY = 16; 115 116 /** 117 * Safe conversion from {@link java.io.File} to {@link java.nio.file.Path}. 118 * 119 * @param f 120 * {@code File} to be converted to {@code Path} 121 * @return the path represented by the file 122 * @throws java.io.IOException 123 * in case the path represented by the file is not valid ( 124 * {@link java.nio.file.InvalidPathException}) 125 * @since 4.10 126 */ 127 public static Path toPath(File f) throws IOException { 128 try { 129 return f.toPath(); 130 } catch (InvalidPathException ex) { 131 throw new IOException(ex); 132 } 133 } 134 135 /** 136 * Delete file or empty folder 137 * 138 * @param f 139 * {@code File} to be deleted 140 * @throws java.io.IOException 141 * if deletion of {@code f} fails. This may occur if {@code f} 142 * didn't exist when the method was called. This can therefore 143 * cause java.io.IOExceptions during race conditions when 144 * multiple concurrent threads all try to delete the same file. 145 */ 146 public static void delete(File f) throws IOException { 147 delete(f, NONE); 148 } 149 150 /** 151 * Delete file or folder 152 * 153 * @param f 154 * {@code File} to be deleted 155 * @param options 156 * deletion options, {@code RECURSIVE} for recursive deletion of 157 * a subtree, {@code RETRY} to retry when deletion failed. 158 * Retrying may help if the underlying file system doesn't allow 159 * deletion of files being read by another thread. 160 * @throws java.io.IOException 161 * if deletion of {@code f} fails. This may occur if {@code f} 162 * didn't exist when the method was called. This can therefore 163 * cause java.io.IOExceptions during race conditions when 164 * multiple concurrent threads all try to delete the same file. 165 * This exception is not thrown when IGNORE_ERRORS is set. 166 */ 167 public static void delete(File f, int options) throws IOException { 168 FS fs = FS.DETECTED; 169 if ((options & SKIP_MISSING) != 0 && !fs.exists(f)) 170 return; 171 172 if ((options & RECURSIVE) != 0 && fs.isDirectory(f)) { 173 final File[] items = f.listFiles(); 174 if (items != null) { 175 List<File> files = new ArrayList<>(); 176 List<File> dirs = new ArrayList<>(); 177 for (File c : items) 178 if (c.isFile()) 179 files.add(c); 180 else 181 dirs.add(c); 182 // Try to delete files first, otherwise options 183 // EMPTY_DIRECTORIES_ONLY|RECURSIVE will delete empty 184 // directories before aborting, depending on order. 185 for (File file : files) 186 delete(file, options); 187 for (File d : dirs) 188 delete(d, options); 189 } 190 } 191 192 boolean delete = false; 193 if ((options & EMPTY_DIRECTORIES_ONLY) != 0) { 194 if (f.isDirectory()) { 195 delete = true; 196 } else { 197 if ((options & IGNORE_ERRORS) == 0) 198 throw new IOException(MessageFormat.format( 199 JGitText.get().deleteFileFailed, 200 f.getAbsolutePath())); 201 } 202 } else { 203 delete = true; 204 } 205 206 if (delete && !f.delete()) { 207 if ((options & RETRY) != 0 && fs.exists(f)) { 208 for (int i = 1; i < 10; i++) { 209 try { 210 Thread.sleep(100); 211 } catch (InterruptedException e) { 212 // ignore 213 } 214 if (f.delete()) 215 return; 216 } 217 } 218 if ((options & IGNORE_ERRORS) == 0) 219 throw new IOException(MessageFormat.format( 220 JGitText.get().deleteFileFailed, f.getAbsolutePath())); 221 } 222 } 223 224 /** 225 * Rename a file or folder. If the rename fails and if we are running on a 226 * filesystem where it makes sense to repeat a failing rename then repeat 227 * the rename operation up to 9 times with 100ms sleep time between two 228 * calls. Furthermore if the destination exists and is directory hierarchy 229 * with only directories in it, the whole directory hierarchy will be 230 * deleted. If the target represents a non-empty directory structure, empty 231 * subdirectories within that structure may or may not be deleted even if 232 * the method fails. Furthermore if the destination exists and is a file 233 * then the file will be deleted and then the rename is retried. 234 * <p> 235 * This operation is <em>not</em> atomic. 236 * 237 * @see FS#retryFailedLockFileCommit() 238 * @param src 239 * the old {@code File} 240 * @param dst 241 * the new {@code File} 242 * @throws java.io.IOException 243 * if the rename has failed 244 * @since 3.0 245 */ 246 public static void rename(File src, File dst) 247 throws IOException { 248 rename(src, dst, StandardCopyOption.REPLACE_EXISTING); 249 } 250 251 /** 252 * Rename a file or folder using the passed 253 * {@link java.nio.file.CopyOption}s. If the rename fails and if we are 254 * running on a filesystem where it makes sense to repeat a failing rename 255 * then repeat the rename operation up to 9 times with 100ms sleep time 256 * between two calls. Furthermore if the destination exists and is a 257 * directory hierarchy with only directories in it, the whole directory 258 * hierarchy will be deleted. If the target represents a non-empty directory 259 * structure, empty subdirectories within that structure may or may not be 260 * deleted even if the method fails. Furthermore if the destination exists 261 * and is a file then the file will be replaced if 262 * {@link java.nio.file.StandardCopyOption#REPLACE_EXISTING} has been set. 263 * If {@link java.nio.file.StandardCopyOption#ATOMIC_MOVE} has been set the 264 * rename will be done atomically or fail with an 265 * {@link java.nio.file.AtomicMoveNotSupportedException} 266 * 267 * @param src 268 * the old file 269 * @param dst 270 * the new file 271 * @param options 272 * options to pass to 273 * {@link java.nio.file.Files#move(java.nio.file.Path, java.nio.file.Path, CopyOption...)} 274 * @throws java.nio.file.AtomicMoveNotSupportedException 275 * if file cannot be moved as an atomic file system operation 276 * @throws java.io.IOException 277 * @since 4.1 278 */ 279 public static void rename(final File src, final File dst, 280 CopyOption... options) 281 throws AtomicMoveNotSupportedException, IOException { 282 int attempts = FS.DETECTED.retryFailedLockFileCommit() ? 10 : 1; 283 while (--attempts >= 0) { 284 try { 285 Files.move(toPath(src), toPath(dst), options); 286 return; 287 } catch (AtomicMoveNotSupportedException e) { 288 throw e; 289 } catch (IOException e) { 290 try { 291 if (!dst.delete()) { 292 delete(dst, EMPTY_DIRECTORIES_ONLY | RECURSIVE); 293 } 294 // On *nix there is no try, you do or do not 295 Files.move(toPath(src), toPath(dst), options); 296 return; 297 } catch (IOException e2) { 298 // ignore and continue retry 299 } 300 } 301 try { 302 Thread.sleep(100); 303 } catch (InterruptedException e) { 304 throw new IOException( 305 MessageFormat.format(JGitText.get().renameFileFailed, 306 src.getAbsolutePath(), dst.getAbsolutePath())); 307 } 308 } 309 throw new IOException( 310 MessageFormat.format(JGitText.get().renameFileFailed, 311 src.getAbsolutePath(), dst.getAbsolutePath())); 312 } 313 314 /** 315 * Creates the directory named by this abstract pathname. 316 * 317 * @param d 318 * directory to be created 319 * @throws java.io.IOException 320 * if creation of {@code d} fails. This may occur if {@code d} 321 * did exist when the method was called. This can therefore 322 * cause java.io.IOExceptions during race conditions when 323 * multiple concurrent threads all try to create the same 324 * directory. 325 */ 326 public static void mkdir(File d) 327 throws IOException { 328 mkdir(d, false); 329 } 330 331 /** 332 * Creates the directory named by this abstract pathname. 333 * 334 * @param d 335 * directory to be created 336 * @param skipExisting 337 * if {@code true} skip creation of the given directory if it 338 * already exists in the file system 339 * @throws java.io.IOException 340 * if creation of {@code d} fails. This may occur if {@code d} 341 * did exist when the method was called. This can therefore 342 * cause java.io.IOExceptions during race conditions when 343 * multiple concurrent threads all try to create the same 344 * directory. 345 */ 346 public static void mkdir(File d, boolean skipExisting) 347 throws IOException { 348 if (!d.mkdir()) { 349 if (skipExisting && d.isDirectory()) 350 return; 351 throw new IOException(MessageFormat.format( 352 JGitText.get().mkDirFailed, d.getAbsolutePath())); 353 } 354 } 355 356 /** 357 * Creates the directory named by this abstract pathname, including any 358 * necessary but nonexistent parent directories. Note that if this operation 359 * fails it may have succeeded in creating some of the necessary parent 360 * directories. 361 * 362 * @param d 363 * directory to be created 364 * @throws java.io.IOException 365 * if creation of {@code d} fails. This may occur if {@code d} 366 * did exist when the method was called. This can therefore 367 * cause java.io.IOExceptions during race conditions when 368 * multiple concurrent threads all try to create the same 369 * directory. 370 */ 371 public static void mkdirs(File d) throws IOException { 372 mkdirs(d, false); 373 } 374 375 /** 376 * Creates the directory named by this abstract pathname, including any 377 * necessary but nonexistent parent directories. Note that if this operation 378 * fails it may have succeeded in creating some of the necessary parent 379 * directories. 380 * 381 * @param d 382 * directory to be created 383 * @param skipExisting 384 * if {@code true} skip creation of the given directory if it 385 * already exists in the file system 386 * @throws java.io.IOException 387 * if creation of {@code d} fails. This may occur if {@code d} 388 * did exist when the method was called. This can therefore 389 * cause java.io.IOExceptions during race conditions when 390 * multiple concurrent threads all try to create the same 391 * directory. 392 */ 393 public static void mkdirs(File d, boolean skipExisting) 394 throws IOException { 395 if (!d.mkdirs()) { 396 if (skipExisting && d.isDirectory()) 397 return; 398 throw new IOException(MessageFormat.format( 399 JGitText.get().mkDirsFailed, d.getAbsolutePath())); 400 } 401 } 402 403 /** 404 * Atomically creates a new, empty file named by this abstract pathname if 405 * and only if a file with this name does not yet exist. The check for the 406 * existence of the file and the creation of the file if it does not exist 407 * are a single operation that is atomic with respect to all other 408 * filesystem activities that might affect the file. 409 * <p> 410 * Note: this method should not be used for file-locking, as the resulting 411 * protocol cannot be made to work reliably. The 412 * {@link java.nio.channels.FileLock} facility should be used instead. 413 * 414 * @param f 415 * the file to be created 416 * @throws java.io.IOException 417 * if the named file already exists or if an I/O error occurred 418 */ 419 public static void createNewFile(File f) throws IOException { 420 if (!f.createNewFile()) 421 throw new IOException(MessageFormat.format( 422 JGitText.get().createNewFileFailed, f)); 423 } 424 425 /** 426 * Create a symbolic link 427 * 428 * @param path 429 * the path of the symbolic link to create 430 * @param target 431 * the target of the symbolic link 432 * @return the path to the symbolic link 433 * @throws java.io.IOException 434 * @since 4.2 435 */ 436 public static Path createSymLink(File path, String target) 437 throws IOException { 438 Path nioPath = toPath(path); 439 if (Files.exists(nioPath, LinkOption.NOFOLLOW_LINKS)) { 440 BasicFileAttributes attrs = Files.readAttributes(nioPath, 441 BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); 442 if (attrs.isRegularFile() || attrs.isSymbolicLink()) { 443 delete(path); 444 } else { 445 delete(path, EMPTY_DIRECTORIES_ONLY | RECURSIVE); 446 } 447 } 448 if (SystemReader.getInstance().isWindows()) { 449 target = target.replace('/', '\\'); 450 } 451 Path nioTarget = toPath(new File(target)); 452 return Files.createSymbolicLink(nioPath, nioTarget); 453 } 454 455 /** 456 * Read target path of the symlink. 457 * 458 * @param path 459 * a {@link java.io.File} object. 460 * @return target path of the symlink, or null if it is not a symbolic link 461 * @throws java.io.IOException 462 * @since 3.0 463 */ 464 public static String readSymLink(File path) throws IOException { 465 Path nioPath = toPath(path); 466 Path target = Files.readSymbolicLink(nioPath); 467 String targetString = target.toString(); 468 if (SystemReader.getInstance().isWindows()) { 469 targetString = targetString.replace('\\', '/'); 470 } else if (SystemReader.getInstance().isMacOS()) { 471 targetString = Normalizer.normalize(targetString, Form.NFC); 472 } 473 return targetString; 474 } 475 476 /** 477 * Create a temporary directory. 478 * 479 * @param prefix 480 * prefix string 481 * @param suffix 482 * suffix string 483 * @param dir 484 * The parent dir, can be null to use system default temp dir. 485 * @return the temp dir created. 486 * @throws java.io.IOException 487 * @since 3.4 488 */ 489 public static File createTempDir(String prefix, String suffix, File dir) 490 throws IOException { 491 final int RETRIES = 1; // When something bad happens, retry once. 492 for (int i = 0; i < RETRIES; i++) { 493 File tmp = File.createTempFile(prefix, suffix, dir); 494 if (!tmp.delete()) 495 continue; 496 if (!tmp.mkdir()) 497 continue; 498 return tmp; 499 } 500 throw new IOException(JGitText.get().cannotCreateTempDir); 501 } 502 503 /** 504 * Expresses <code>other</code> as a relative file path from 505 * <code>base</code>. File-separator and case sensitivity are based on the 506 * current file system. 507 * 508 * See also 509 * {@link org.eclipse.jgit.util.FileUtils#relativizePath(String, String, String, boolean)}. 510 * 511 * @param base 512 * Base path 513 * @param other 514 * Destination path 515 * @return Relative path from <code>base</code> to <code>other</code> 516 * @since 4.8 517 */ 518 public static String relativizeNativePath(String base, String other) { 519 return FS.DETECTED.relativize(base, other); 520 } 521 522 /** 523 * Expresses <code>other</code> as a relative file path from 524 * <code>base</code>. File-separator and case sensitivity are based on Git's 525 * internal representation of files (which matches Unix). 526 * 527 * See also 528 * {@link org.eclipse.jgit.util.FileUtils#relativizePath(String, String, String, boolean)}. 529 * 530 * @param base 531 * Base path 532 * @param other 533 * Destination path 534 * @return Relative path from <code>base</code> to <code>other</code> 535 * @since 4.8 536 */ 537 public static String relativizeGitPath(String base, String other) { 538 return relativizePath(base, other, "/", false); //$NON-NLS-1$ 539 } 540 541 542 /** 543 * Expresses <code>other</code> as a relative file path from <code>base</code> 544 * <p> 545 * For example, if called with the two following paths : 546 * 547 * <pre> 548 * <code>base = "c:\\Users\\jdoe\\eclipse\\git\\project"</code> 549 * <code>other = "c:\\Users\\jdoe\\eclipse\\git\\another_project\\pom.xml"</code> 550 * </pre> 551 * 552 * This will return "..\\another_project\\pom.xml". 553 * 554 * <p> 555 * <b>Note</b> that this will return the empty String if <code>base</code> 556 * and <code>other</code> are equal. 557 * </p> 558 * 559 * @param base 560 * The path against which <code>other</code> should be 561 * relativized. This will be assumed to denote the path to a 562 * folder and not a file. 563 * @param other 564 * The path that will be made relative to <code>base</code>. 565 * @param dirSeparator 566 * A string that separates components of the path. In practice, this is "/" or "\\". 567 * @param caseSensitive 568 * Whether to consider differently-cased directory names as distinct 569 * @return A relative path that, when resolved against <code>base</code>, 570 * will yield the original <code>other</code>. 571 * @since 4.8 572 */ 573 public static String relativizePath(String base, String other, String dirSeparator, boolean caseSensitive) { 574 if (base.equals(other)) 575 return ""; //$NON-NLS-1$ 576 577 final String[] baseSegments = base.split(Pattern.quote(dirSeparator)); 578 final String[] otherSegments = other.split(Pattern 579 .quote(dirSeparator)); 580 581 int commonPrefix = 0; 582 while (commonPrefix < baseSegments.length 583 && commonPrefix < otherSegments.length) { 584 if (caseSensitive 585 && baseSegments[commonPrefix] 586 .equals(otherSegments[commonPrefix])) 587 commonPrefix++; 588 else if (!caseSensitive 589 && baseSegments[commonPrefix] 590 .equalsIgnoreCase(otherSegments[commonPrefix])) 591 commonPrefix++; 592 else 593 break; 594 } 595 596 final StringBuilder builder = new StringBuilder(); 597 for (int i = commonPrefix; i < baseSegments.length; i++) 598 builder.append("..").append(dirSeparator); //$NON-NLS-1$ 599 for (int i = commonPrefix; i < otherSegments.length; i++) { 600 builder.append(otherSegments[i]); 601 if (i < otherSegments.length - 1) 602 builder.append(dirSeparator); 603 } 604 return builder.toString(); 605 } 606 607 /** 608 * Determine if an IOException is a Stale NFS File Handle 609 * 610 * @param ioe 611 * an {@link java.io.IOException} object. 612 * @return a boolean true if the IOException is a Stale NFS FIle Handle 613 * @since 4.1 614 */ 615 public static boolean isStaleFileHandle(IOException ioe) { 616 String msg = ioe.getMessage(); 617 return msg != null 618 && msg.toLowerCase(Locale.ROOT) 619 .matches("stale .*file .*handle"); //$NON-NLS-1$ 620 } 621 622 /** 623 * Determine if a throwable or a cause in its causal chain is a Stale NFS 624 * File Handle 625 * 626 * @param throwable 627 * a {@link java.lang.Throwable} object. 628 * @return a boolean true if the throwable or a cause in its causal chain is 629 * a Stale NFS File Handle 630 * @since 4.7 631 */ 632 public static boolean isStaleFileHandleInCausalChain(Throwable throwable) { 633 while (throwable != null) { 634 if (throwable instanceof IOException 635 && isStaleFileHandle((IOException) throwable)) { 636 return true; 637 } 638 throwable = throwable.getCause(); 639 } 640 return false; 641 } 642 643 /** 644 * @param file 645 * @return {@code true} if the passed file is a symbolic link 646 */ 647 static boolean isSymlink(File file) { 648 return Files.isSymbolicLink(file.toPath()); 649 } 650 651 /** 652 * @param file 653 * @return lastModified attribute for given file, not following symbolic 654 * links 655 * @throws IOException 656 */ 657 static long lastModified(File file) throws IOException { 658 return Files.getLastModifiedTime(toPath(file), LinkOption.NOFOLLOW_LINKS) 659 .toMillis(); 660 } 661 662 /** 663 * @param file 664 * @param time 665 * @throws IOException 666 */ 667 static void setLastModified(File file, long time) throws IOException { 668 Files.setLastModifiedTime(toPath(file), FileTime.fromMillis(time)); 669 } 670 671 /** 672 * @param file 673 * @return {@code true} if the given file exists, not following symbolic 674 * links 675 */ 676 static boolean exists(File file) { 677 return Files.exists(file.toPath(), LinkOption.NOFOLLOW_LINKS); 678 } 679 680 /** 681 * @param file 682 * @return {@code true} if the given file is hidden 683 * @throws IOException 684 */ 685 static boolean isHidden(File file) throws IOException { 686 return Files.isHidden(toPath(file)); 687 } 688 689 /** 690 * Set a file hidden (on Windows) 691 * 692 * @param file 693 * a {@link java.io.File} object. 694 * @param hidden 695 * a boolean. 696 * @throws java.io.IOException 697 * @since 4.1 698 */ 699 public static void setHidden(File file, boolean hidden) throws IOException { 700 Files.setAttribute(toPath(file), "dos:hidden", Boolean.valueOf(hidden), //$NON-NLS-1$ 701 LinkOption.NOFOLLOW_LINKS); 702 } 703 704 /** 705 * Get file length 706 * 707 * @param file 708 * a {@link java.io.File}. 709 * @return length of the given file 710 * @throws java.io.IOException 711 * @since 4.1 712 */ 713 public static long getLength(File file) throws IOException { 714 Path nioPath = toPath(file); 715 if (Files.isSymbolicLink(nioPath)) 716 return Files.readSymbolicLink(nioPath).toString() 717 .getBytes(UTF_8).length; 718 return Files.size(nioPath); 719 } 720 721 /** 722 * @param file 723 * @return {@code true} if the given file is a directory, not following 724 * symbolic links 725 */ 726 static boolean isDirectory(File file) { 727 return Files.isDirectory(file.toPath(), LinkOption.NOFOLLOW_LINKS); 728 } 729 730 /** 731 * @param file 732 * @return {@code true} if the given file is a file, not following symbolic 733 * links 734 */ 735 static boolean isFile(File file) { 736 return Files.isRegularFile(file.toPath(), LinkOption.NOFOLLOW_LINKS); 737 } 738 739 /** 740 * Whether the given file can be executed. 741 * 742 * @param file 743 * a {@link java.io.File} object. 744 * @return {@code true} if the given file can be executed. 745 * @since 4.1 746 */ 747 public static boolean canExecute(File file) { 748 if (!isFile(file)) { 749 return false; 750 } 751 return Files.isExecutable(file.toPath()); 752 } 753 754 /** 755 * @param fs 756 * @param file 757 * @return non null attributes object 758 */ 759 static Attributes getFileAttributesBasic(FS fs, File file) { 760 try { 761 Path nioPath = toPath(file); 762 BasicFileAttributes readAttributes = nioPath 763 .getFileSystem() 764 .provider() 765 .getFileAttributeView(nioPath, 766 BasicFileAttributeView.class, 767 LinkOption.NOFOLLOW_LINKS).readAttributes(); 768 Attributes attributes = new Attributes(fs, file, 769 true, 770 readAttributes.isDirectory(), 771 fs.supportsExecute() ? file.canExecute() : false, 772 readAttributes.isSymbolicLink(), 773 readAttributes.isRegularFile(), // 774 readAttributes.creationTime().toMillis(), // 775 readAttributes.lastModifiedTime().toMillis(), 776 readAttributes.isSymbolicLink() ? Constants 777 .encode(readSymLink(file)).length 778 : readAttributes.size()); 779 return attributes; 780 } catch (IOException e) { 781 return new Attributes(file, fs); 782 } 783 } 784 785 /** 786 * Get file system attributes for the given file. 787 * 788 * @param fs 789 * a {@link org.eclipse.jgit.util.FS} object. 790 * @param file 791 * a {@link java.io.File}. 792 * @return file system attributes for the given file. 793 * @since 4.1 794 */ 795 public static Attributes getFileAttributesPosix(FS fs, File file) { 796 try { 797 Path nioPath = toPath(file); 798 PosixFileAttributes readAttributes = nioPath 799 .getFileSystem() 800 .provider() 801 .getFileAttributeView(nioPath, 802 PosixFileAttributeView.class, 803 LinkOption.NOFOLLOW_LINKS).readAttributes(); 804 Attributes attributes = new Attributes( 805 fs, 806 file, 807 true, // 808 readAttributes.isDirectory(), // 809 readAttributes.permissions().contains( 810 PosixFilePermission.OWNER_EXECUTE), 811 readAttributes.isSymbolicLink(), 812 readAttributes.isRegularFile(), // 813 readAttributes.creationTime().toMillis(), // 814 readAttributes.lastModifiedTime().toMillis(), 815 readAttributes.size()); 816 return attributes; 817 } catch (IOException e) { 818 return new Attributes(file, fs); 819 } 820 } 821 822 /** 823 * NFC normalize a file (on Mac), otherwise do nothing 824 * 825 * @param file 826 * a {@link java.io.File}. 827 * @return on Mac: NFC normalized {@link java.io.File}, otherwise the passed 828 * file 829 * @since 4.1 830 */ 831 public static File normalize(File file) { 832 if (SystemReader.getInstance().isMacOS()) { 833 // TODO: Would it be faster to check with isNormalized first 834 // assuming normalized paths are much more common 835 String normalized = Normalizer.normalize(file.getPath(), 836 Normalizer.Form.NFC); 837 return new File(normalized); 838 } 839 return file; 840 } 841 842 /** 843 * On Mac: get NFC normalized form of given name, otherwise the given name. 844 * 845 * @param name 846 * a {@link java.lang.String} object. 847 * @return on Mac: NFC normalized form of given name 848 * @since 4.1 849 */ 850 public static String normalize(String name) { 851 if (SystemReader.getInstance().isMacOS()) { 852 if (name == null) 853 return null; 854 return Normalizer.normalize(name, Normalizer.Form.NFC); 855 } 856 return name; 857 } 858 859 /** 860 * Best-effort variation of {@link java.io.File#getCanonicalFile()} 861 * returning the input file if the file cannot be canonicalized instead of 862 * throwing {@link java.io.IOException}. 863 * 864 * @param file 865 * to be canonicalized; may be {@code null} 866 * @return canonicalized file, or the unchanged input file if 867 * canonicalization failed or if {@code file == null} 868 * @throws java.lang.SecurityException 869 * if {@link java.io.File#getCanonicalFile()} throws one 870 * @since 4.2 871 */ 872 public static File canonicalize(File file) { 873 if (file == null) { 874 return null; 875 } 876 try { 877 return file.getCanonicalFile(); 878 } catch (IOException e) { 879 return file; 880 } 881 } 882 883 /** 884 * Convert a path to String, replacing separators as necessary. 885 * 886 * @param file 887 * a {@link java.io.File}. 888 * @return file's path as a String 889 * @since 4.10 890 */ 891 public static String pathToString(File file) { 892 final String path = file.getPath(); 893 if (SystemReader.getInstance().isWindows()) { 894 return path.replace('\\', '/'); 895 } 896 return path; 897 } 898 }