001/* 002 * Copyright (C) 2013 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.google.common.io; 018 019import static com.google.common.base.Preconditions.checkNotNull; 020import static com.google.common.collect.Iterables.getOnlyElement; 021import static java.nio.file.LinkOption.NOFOLLOW_LINKS; 022import static java.util.Objects.requireNonNull; 023 024import com.google.common.annotations.GwtIncompatible; 025import com.google.common.annotations.J2ktIncompatible; 026import com.google.common.base.Optional; 027import com.google.common.base.Predicate; 028import com.google.common.collect.ImmutableList; 029import com.google.common.graph.Traverser; 030import com.google.j2objc.annotations.J2ObjCIncompatible; 031import java.io.IOException; 032import java.io.InputStream; 033import java.io.OutputStream; 034import java.nio.channels.Channels; 035import java.nio.channels.SeekableByteChannel; 036import java.nio.charset.Charset; 037import java.nio.file.DirectoryIteratorException; 038import java.nio.file.DirectoryStream; 039import java.nio.file.FileAlreadyExistsException; 040import java.nio.file.FileSystemException; 041import java.nio.file.Files; 042import java.nio.file.LinkOption; 043import java.nio.file.NoSuchFileException; 044import java.nio.file.NotDirectoryException; 045import java.nio.file.OpenOption; 046import java.nio.file.Path; 047import java.nio.file.SecureDirectoryStream; 048import java.nio.file.StandardOpenOption; 049import java.nio.file.attribute.BasicFileAttributeView; 050import java.nio.file.attribute.BasicFileAttributes; 051import java.nio.file.attribute.FileAttribute; 052import java.nio.file.attribute.FileTime; 053import java.util.ArrayList; 054import java.util.Arrays; 055import java.util.Collection; 056import java.util.stream.Stream; 057import org.checkerframework.checker.nullness.qual.Nullable; 058 059/** 060 * Static utilities for use with {@link Path} instances, intended to complement {@link Files}. 061 * 062 * <p>Many methods provided by Guava's {@code Files} class for {@link java.io.File} instances are 063 * now available via the JDK's {@link java.nio.file.Files} class for {@code Path} - check the JDK's 064 * class if a sibling method from {@code Files} appears to be missing from this class. 065 * 066 * @since 33.4.0 (but since 21.0 in the JRE flavor) 067 * @author Colin Decker 068 */ 069@J2ktIncompatible 070@GwtIncompatible 071@J2ObjCIncompatible // java.nio.file 072@IgnoreJRERequirement // Users will use this only if they're already using Path. 073public final class MoreFiles { 074 075 private MoreFiles() {} 076 077 /** 078 * Returns a view of the given {@code path} as a {@link ByteSource}. 079 * 080 * <p>Any {@linkplain OpenOption open options} provided are used when opening streams to the file 081 * and may affect the behavior of the returned source and the streams it provides. See {@link 082 * StandardOpenOption} for the standard options that may be provided. Providing no options is 083 * equivalent to providing the {@link StandardOpenOption#READ READ} option. 084 */ 085 public static ByteSource asByteSource(Path path, OpenOption... options) { 086 return new PathByteSource(path, options); 087 } 088 089 @IgnoreJRERequirement // *should* be redundant with the one on MoreFiles itself 090 private static final class PathByteSource extends 091 ByteSource 092 { 093 094 private static final LinkOption[] FOLLOW_LINKS = {}; 095 096 private final Path path; 097 private final OpenOption[] options; 098 private final boolean followLinks; 099 100 private PathByteSource(Path path, OpenOption... options) { 101 this.path = checkNotNull(path); 102 this.options = options.clone(); 103 this.followLinks = followLinks(this.options); 104 // TODO(cgdecker): validate the provided options... for example, just WRITE seems wrong 105 } 106 107 private static boolean followLinks(OpenOption[] options) { 108 for (OpenOption option : options) { 109 if (option == NOFOLLOW_LINKS) { 110 return false; 111 } 112 } 113 return true; 114 } 115 116 @Override 117 public InputStream openStream() throws IOException { 118 return Files.newInputStream(path, options); 119 } 120 121 private BasicFileAttributes readAttributes() throws IOException { 122 return Files.readAttributes( 123 path, 124 BasicFileAttributes.class, 125 followLinks ? FOLLOW_LINKS : new LinkOption[] {NOFOLLOW_LINKS}); 126 } 127 128 @Override 129 public Optional<Long> sizeIfKnown() { 130 BasicFileAttributes attrs; 131 try { 132 attrs = readAttributes(); 133 } catch (IOException e) { 134 // Failed to get attributes; we don't know the size. 135 return Optional.absent(); 136 } 137 138 // Don't return a size for directories or symbolic links; their sizes are implementation 139 // specific and they can't be read as bytes using the read methods anyway. 140 if (attrs.isDirectory() || attrs.isSymbolicLink()) { 141 return Optional.absent(); 142 } 143 144 return Optional.of(attrs.size()); 145 } 146 147 @Override 148 public long size() throws IOException { 149 BasicFileAttributes attrs = readAttributes(); 150 151 // Don't return a size for directories or symbolic links; their sizes are implementation 152 // specific and they can't be read as bytes using the read methods anyway. 153 if (attrs.isDirectory()) { 154 throw new IOException("can't read: is a directory"); 155 } else if (attrs.isSymbolicLink()) { 156 throw new IOException("can't read: is a symbolic link"); 157 } 158 159 return attrs.size(); 160 } 161 162 @Override 163 public byte[] read() throws IOException { 164 try (SeekableByteChannel channel = Files.newByteChannel(path, options)) { 165 return ByteStreams.toByteArray(Channels.newInputStream(channel), channel.size()); 166 } 167 } 168 169 @Override 170 public CharSource asCharSource(Charset charset) { 171 if (options.length == 0) { 172 // If no OpenOptions were passed, delegate to Files.lines, which could have performance 173 // advantages. (If OpenOptions were passed we can't, because Files.lines doesn't have an 174 // overload taking OpenOptions, meaning we can't guarantee the same behavior w.r.t. things 175 // like following/not following symlinks.) 176 return new AsCharSource(charset) { 177 @SuppressWarnings({ 178 "FilesLinesLeak", // the user needs to close it in this case 179 /* 180 * If users use this when they shouldn't, we hope that NewApi will catch subsequent 181 * Stream calls. 182 * 183 * Anyway, this is just an override that is no more dangerous than the supermethod. 184 */ 185 "Java7ApiChecker", 186 }) 187 @Override 188 public Stream<String> lines() throws IOException { 189 return Files.lines(path, charset); 190 } 191 }; 192 } 193 194 return super.asCharSource(charset); 195 } 196 197 @Override 198 public String toString() { 199 return "MoreFiles.asByteSource(" + path + ", " + Arrays.toString(options) + ")"; 200 } 201 } 202 203 /** 204 * Returns a view of the given {@code path} as a {@link ByteSink}. 205 * 206 * <p>Any {@linkplain OpenOption open options} provided are used when opening streams to the file 207 * and may affect the behavior of the returned sink and the streams it provides. See {@link 208 * StandardOpenOption} for the standard options that may be provided. Providing no options is 209 * equivalent to providing the {@link StandardOpenOption#CREATE CREATE}, {@link 210 * StandardOpenOption#TRUNCATE_EXISTING TRUNCATE_EXISTING} and {@link StandardOpenOption#WRITE 211 * WRITE} options. 212 */ 213 public static ByteSink asByteSink(Path path, OpenOption... options) { 214 return new PathByteSink(path, options); 215 } 216 217 @IgnoreJRERequirement // *should* be redundant with the one on MoreFiles itself 218 private static final class PathByteSink extends ByteSink { 219 220 private final Path path; 221 private final OpenOption[] options; 222 223 private PathByteSink(Path path, OpenOption... options) { 224 this.path = checkNotNull(path); 225 this.options = options.clone(); 226 // TODO(cgdecker): validate the provided options... for example, just READ seems wrong 227 } 228 229 @Override 230 public OutputStream openStream() throws IOException { 231 return Files.newOutputStream(path, options); 232 } 233 234 @Override 235 public String toString() { 236 return "MoreFiles.asByteSink(" + path + ", " + Arrays.toString(options) + ")"; 237 } 238 } 239 240 /** 241 * Returns a view of the given {@code path} as a {@link CharSource} using the given {@code 242 * charset}. 243 * 244 * <p>Any {@linkplain OpenOption open options} provided are used when opening streams to the file 245 * and may affect the behavior of the returned source and the streams it provides. See {@link 246 * StandardOpenOption} for the standard options that may be provided. Providing no options is 247 * equivalent to providing the {@link StandardOpenOption#READ READ} option. 248 */ 249 public static CharSource asCharSource(Path path, Charset charset, OpenOption... options) { 250 return asByteSource(path, options).asCharSource(charset); 251 } 252 253 /** 254 * Returns a view of the given {@code path} as a {@link CharSink} using the given {@code charset}. 255 * 256 * <p>Any {@linkplain OpenOption open options} provided are used when opening streams to the file 257 * and may affect the behavior of the returned sink and the streams it provides. See {@link 258 * StandardOpenOption} for the standard options that may be provided. Providing no options is 259 * equivalent to providing the {@link StandardOpenOption#CREATE CREATE}, {@link 260 * StandardOpenOption#TRUNCATE_EXISTING TRUNCATE_EXISTING} and {@link StandardOpenOption#WRITE 261 * WRITE} options. 262 */ 263 public static CharSink asCharSink(Path path, Charset charset, OpenOption... options) { 264 return asByteSink(path, options).asCharSink(charset); 265 } 266 267 /** 268 * Returns an immutable list of paths to the files contained in the given directory. 269 * 270 * @throws NoSuchFileException if the file does not exist <i>(optional specific exception)</i> 271 * @throws NotDirectoryException if the file could not be opened because it is not a directory 272 * <i>(optional specific exception)</i> 273 * @throws IOException if an I/O error occurs 274 */ 275 public static ImmutableList<Path> listFiles(Path dir) throws IOException { 276 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { 277 return ImmutableList.copyOf(stream); 278 } catch (DirectoryIteratorException e) { 279 throw e.getCause(); 280 } 281 } 282 283 /** 284 * Returns a {@link Traverser} instance for the file and directory tree. The returned traverser 285 * starts from a {@link Path} and will return all files and directories it encounters. 286 * 287 * <p>The returned traverser attempts to avoid following symbolic links to directories. However, 288 * the traverser cannot guarantee that it will not follow symbolic links to directories as it is 289 * possible for a directory to be replaced with a symbolic link between checking if the file is a 290 * directory and actually reading the contents of that directory. 291 * 292 * <p>If the {@link Path} passed to one of the traversal methods does not exist or is not a 293 * directory, no exception will be thrown and the returned {@link Iterable} will contain a single 294 * element: that path. 295 * 296 * <p>{@link DirectoryIteratorException} may be thrown when iterating {@link Iterable} instances 297 * created by this traverser if an {@link IOException} is thrown by a call to {@link 298 * #listFiles(Path)}. 299 * 300 * <p>Example: {@code MoreFiles.fileTraverser().depthFirstPreOrder(Paths.get("/"))} may return the 301 * following paths: {@code ["/", "/etc", "/etc/config.txt", "/etc/fonts", "/home", "/home/alice", 302 * ...]} 303 * 304 * @since 23.5 305 */ 306 public static Traverser<Path> fileTraverser() { 307 return Traverser.forTree(MoreFiles::fileTreeChildren); 308 } 309 310 private static Iterable<Path> fileTreeChildren(Path dir) { 311 if (Files.isDirectory(dir, NOFOLLOW_LINKS)) { 312 try { 313 return listFiles(dir); 314 } catch (IOException e) { 315 // the exception thrown when iterating a DirectoryStream if an I/O exception occurs 316 throw new DirectoryIteratorException(e); 317 } 318 } 319 return ImmutableList.of(); 320 } 321 322 /** 323 * Returns a predicate that returns the result of {@link java.nio.file.Files#isDirectory(Path, 324 * LinkOption...)} on input paths with the given link options. 325 */ 326 public static Predicate<Path> isDirectory(LinkOption... options) { 327 final LinkOption[] optionsCopy = options.clone(); 328 return new Predicate<Path>() { 329 @Override 330 public boolean apply(Path input) { 331 return Files.isDirectory(input, optionsCopy); 332 } 333 334 @Override 335 public String toString() { 336 return "MoreFiles.isDirectory(" + Arrays.toString(optionsCopy) + ")"; 337 } 338 }; 339 } 340 341 /** Returns whether or not the file with the given name in the given dir is a directory. */ 342 private static boolean isDirectory( 343 SecureDirectoryStream<Path> dir, Path name, LinkOption... options) throws IOException { 344 return dir.getFileAttributeView(name, BasicFileAttributeView.class, options) 345 .readAttributes() 346 .isDirectory(); 347 } 348 349 /** 350 * Returns a predicate that returns the result of {@link java.nio.file.Files#isRegularFile(Path, 351 * LinkOption...)} on input paths with the given link options. 352 */ 353 public static Predicate<Path> isRegularFile(LinkOption... options) { 354 final LinkOption[] optionsCopy = options.clone(); 355 return new Predicate<Path>() { 356 @Override 357 public boolean apply(Path input) { 358 return Files.isRegularFile(input, optionsCopy); 359 } 360 361 @Override 362 public String toString() { 363 return "MoreFiles.isRegularFile(" + Arrays.toString(optionsCopy) + ")"; 364 } 365 }; 366 } 367 368 /** 369 * Returns true if the files located by the given paths exist, are not directories, and contain 370 * the same bytes. 371 * 372 * @throws IOException if an I/O error occurs 373 * @since 22.0 374 */ 375 public static boolean equal(Path path1, Path path2) throws IOException { 376 checkNotNull(path1); 377 checkNotNull(path2); 378 if (Files.isSameFile(path1, path2)) { 379 return true; 380 } 381 382 /* 383 * Some operating systems may return zero as the length for files denoting system-dependent 384 * entities such as devices or pipes, in which case we must fall back on comparing the bytes 385 * directly. 386 */ 387 ByteSource source1 = asByteSource(path1); 388 ByteSource source2 = asByteSource(path2); 389 long len1 = source1.sizeIfKnown().or(0L); 390 long len2 = source2.sizeIfKnown().or(0L); 391 if (len1 != 0 && len2 != 0 && len1 != len2) { 392 return false; 393 } 394 return source1.contentEquals(source2); 395 } 396 397 /** 398 * Like the unix command of the same name, creates an empty file or updates the last modified 399 * timestamp of the existing file at the given path to the current system time. 400 */ 401 @SuppressWarnings("GoodTime") // reading system time without TimeSource 402 public static void touch(Path path) throws IOException { 403 checkNotNull(path); 404 405 try { 406 Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis())); 407 } catch (NoSuchFileException e) { 408 try { 409 Files.createFile(path); 410 } catch (FileAlreadyExistsException ignore) { 411 // The file didn't exist when we called setLastModifiedTime, but it did when we called 412 // createFile, so something else created the file in between. The end result is 413 // what we wanted: a new file that probably has its last modified time set to approximately 414 // now. Or it could have an arbitrary last modified time set by the creator, but that's no 415 // different than if another process set its last modified time to something else after we 416 // created it here. 417 } 418 } 419 } 420 421 /** 422 * Creates any necessary but nonexistent parent directories of the specified path. Note that if 423 * this operation fails, it may have succeeded in creating some (but not all) of the necessary 424 * parent directories. The parent directory is created with the given {@code attrs}. 425 * 426 * @throws IOException if an I/O error occurs, or if any necessary but nonexistent parent 427 * directories of the specified file could not be created. 428 */ 429 public static void createParentDirectories(Path path, FileAttribute<?>... attrs) 430 throws IOException { 431 // Interestingly, unlike File.getCanonicalFile(), Path/Files provides no way of getting the 432 // canonical (absolute, normalized, symlinks resolved, etc.) form of a path to a nonexistent 433 // file. getCanonicalFile() can at least get the canonical form of the part of the path which 434 // actually exists and then append the normalized remainder of the path to that. 435 Path normalizedAbsolutePath = path.toAbsolutePath().normalize(); 436 Path parent = normalizedAbsolutePath.getParent(); 437 if (parent == null) { 438 // The given directory is a filesystem root. All zero of its ancestors exist. This doesn't 439 // mean that the root itself exists -- consider x:\ on a Windows machine without such a 440 // drive -- or even that the caller can create it, but this method makes no such guarantees 441 // even for non-root files. 442 return; 443 } 444 445 // Check if the parent is a directory first because createDirectories will fail if the parent 446 // exists and is a symlink to a directory... we'd like for this to succeed in that case. 447 // (I'm kind of surprised that createDirectories would fail in that case; doesn't seem like 448 // what you'd want to happen.) 449 if (!Files.isDirectory(parent)) { 450 Files.createDirectories(parent, attrs); 451 if (!Files.isDirectory(parent)) { 452 throw new IOException("Unable to create parent directories of " + path); 453 } 454 } 455 } 456 457 /** 458 * Returns the <a href="http://en.wikipedia.org/wiki/Filename_extension">file extension</a> for 459 * the file at the given path, or the empty string if the file has no extension. The result does 460 * not include the '{@code .}'. 461 * 462 * <p><b>Note:</b> This method simply returns everything after the last '{@code .}' in the file's 463 * name as determined by {@link Path#getFileName}. It does not account for any filesystem-specific 464 * behavior that the {@link Path} API does not already account for. For example, on NTFS it will 465 * report {@code "txt"} as the extension for the filename {@code "foo.exe:.txt"} even though NTFS 466 * will drop the {@code ":.txt"} part of the name when the file is actually created on the 467 * filesystem due to NTFS's <a 468 * href="https://learn.microsoft.com/en-us/archive/blogs/askcore/alternate-data-streams-in-ntfs">Alternate 469 * Data Streams</a>. 470 */ 471 public static String getFileExtension(Path path) { 472 Path name = path.getFileName(); 473 474 // null for empty paths and root-only paths 475 if (name == null) { 476 return ""; 477 } 478 479 String fileName = name.toString(); 480 int dotIndex = fileName.lastIndexOf('.'); 481 return dotIndex == -1 ? "" : fileName.substring(dotIndex + 1); 482 } 483 484 /** 485 * Returns the file name without its <a 486 * href="http://en.wikipedia.org/wiki/Filename_extension">file extension</a> or path. This is 487 * similar to the {@code basename} unix command. The result does not include the '{@code .}'. 488 */ 489 public static String getNameWithoutExtension(Path path) { 490 Path name = path.getFileName(); 491 492 // null for empty paths and root-only paths 493 if (name == null) { 494 return ""; 495 } 496 497 String fileName = name.toString(); 498 int dotIndex = fileName.lastIndexOf('.'); 499 return dotIndex == -1 ? fileName : fileName.substring(0, dotIndex); 500 } 501 502 /** 503 * Deletes the file or directory at the given {@code path} recursively. Deletes symbolic links, 504 * not their targets (subject to the caveat below). 505 * 506 * <p>If an I/O exception occurs attempting to read, open or delete any file under the given 507 * directory, this method skips that file and continues. All such exceptions are collected and, 508 * after attempting to delete all files, an {@code IOException} is thrown containing those 509 * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}. 510 * 511 * <h2>Warning: Security of recursive deletes</h2> 512 * 513 * <p>On a file system that supports symbolic links and does <i>not</i> support {@link 514 * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories 515 * that are <i>outside</i> the directory being deleted. This can happen if, after checking that a 516 * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to 517 * an outside directory before the call that opens the directory to read its entries. 518 * 519 * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't 520 * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway, 521 * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior. 522 * 523 * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific exception)</i> 524 * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be 525 * guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not 526 * specified 527 * @throws IOException if {@code path} or any file in the subtree rooted at it can't be deleted 528 * for any reason 529 */ 530 public static void deleteRecursively(Path path, RecursiveDeleteOption... options) 531 throws IOException { 532 Path parentPath = getParentPath(path); 533 if (parentPath == null) { 534 throw new FileSystemException(path.toString(), null, "can't delete recursively"); 535 } 536 537 Collection<IOException> exceptions = null; // created lazily if needed 538 try { 539 boolean sdsSupported = false; 540 try (DirectoryStream<Path> parent = Files.newDirectoryStream(parentPath)) { 541 if (parent instanceof SecureDirectoryStream) { 542 sdsSupported = true; 543 exceptions = 544 deleteRecursivelySecure( 545 (SecureDirectoryStream<Path>) parent, 546 /* 547 * requireNonNull is safe because paths have file names when they have parents, 548 * and we checked for a parent at the beginning of the method. 549 */ 550 requireNonNull(path.getFileName())); 551 } 552 } 553 554 if (!sdsSupported) { 555 checkAllowsInsecure(path, options); 556 exceptions = deleteRecursivelyInsecure(path); 557 } 558 } catch (IOException e) { 559 if (exceptions == null) { 560 throw e; 561 } else { 562 exceptions.add(e); 563 } 564 } 565 566 if (exceptions != null) { 567 throwDeleteFailed(path, exceptions); 568 } 569 } 570 571 /** 572 * Deletes all files within the directory at the given {@code path} {@linkplain #deleteRecursively 573 * recursively}. Does not delete the directory itself. Deletes symbolic links, not their targets 574 * (subject to the caveat below). If {@code path} itself is a symbolic link to a directory, that 575 * link is followed and the contents of the directory it targets are deleted. 576 * 577 * <p>If an I/O exception occurs attempting to read, open or delete any file under the given 578 * directory, this method skips that file and continues. All such exceptions are collected and, 579 * after attempting to delete all files, an {@code IOException} is thrown containing those 580 * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}. 581 * 582 * <h2>Warning: Security of recursive deletes</h2> 583 * 584 * <p>On a file system that supports symbolic links and does <i>not</i> support {@link 585 * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories 586 * that are <i>outside</i> the directory being deleted. This can happen if, after checking that a 587 * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to 588 * an outside directory before the call that opens the directory to read its entries. 589 * 590 * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't 591 * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway, 592 * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior. 593 * 594 * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific exception)</i> 595 * @throws NotDirectoryException if the file at {@code path} is not a directory <i>(optional 596 * specific exception)</i> 597 * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be 598 * guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not 599 * specified 600 * @throws IOException if one or more files can't be deleted for any reason 601 */ 602 public static void deleteDirectoryContents(Path path, RecursiveDeleteOption... options) 603 throws IOException { 604 Collection<IOException> exceptions = null; // created lazily if needed 605 try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { 606 if (stream instanceof SecureDirectoryStream) { 607 SecureDirectoryStream<Path> sds = (SecureDirectoryStream<Path>) stream; 608 exceptions = deleteDirectoryContentsSecure(sds); 609 } else { 610 checkAllowsInsecure(path, options); 611 exceptions = deleteDirectoryContentsInsecure(stream); 612 } 613 } catch (IOException e) { 614 if (exceptions == null) { 615 throw e; 616 } else { 617 exceptions.add(e); 618 } 619 } 620 621 if (exceptions != null) { 622 throwDeleteFailed(path, exceptions); 623 } 624 } 625 626 /** 627 * Secure recursive delete using {@code SecureDirectoryStream}. Returns a collection of exceptions 628 * that occurred or null if no exceptions were thrown. 629 */ 630 private static @Nullable Collection<IOException> deleteRecursivelySecure( 631 SecureDirectoryStream<Path> dir, Path path) { 632 Collection<IOException> exceptions = null; 633 try { 634 if (isDirectory(dir, path, NOFOLLOW_LINKS)) { 635 try (SecureDirectoryStream<Path> childDir = dir.newDirectoryStream(path, NOFOLLOW_LINKS)) { 636 exceptions = deleteDirectoryContentsSecure(childDir); 637 } 638 639 // If exceptions is not null, something went wrong trying to delete the contents of the 640 // directory, so we shouldn't try to delete the directory as it will probably fail. 641 if (exceptions == null) { 642 dir.deleteDirectory(path); 643 } 644 } else { 645 dir.deleteFile(path); 646 } 647 648 return exceptions; 649 } catch (IOException e) { 650 return addException(exceptions, e); 651 } 652 } 653 654 /** 655 * Secure method for deleting the contents of a directory using {@code SecureDirectoryStream}. 656 * Returns a collection of exceptions that occurred or null if no exceptions were thrown. 657 */ 658 private static @Nullable Collection<IOException> deleteDirectoryContentsSecure( 659 SecureDirectoryStream<Path> dir) { 660 Collection<IOException> exceptions = null; 661 try { 662 for (Path path : dir) { 663 exceptions = concat(exceptions, deleteRecursivelySecure(dir, path.getFileName())); 664 } 665 666 return exceptions; 667 } catch (DirectoryIteratorException e) { 668 return addException(exceptions, e.getCause()); 669 } 670 } 671 672 /** 673 * Insecure recursive delete for file systems that don't support {@code SecureDirectoryStream}. 674 * Returns a collection of exceptions that occurred or null if no exceptions were thrown. 675 */ 676 private static @Nullable Collection<IOException> deleteRecursivelyInsecure(Path path) { 677 Collection<IOException> exceptions = null; 678 try { 679 if (Files.isDirectory(path, NOFOLLOW_LINKS)) { 680 try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { 681 exceptions = deleteDirectoryContentsInsecure(stream); 682 } 683 } 684 685 // If exceptions is not null, something went wrong trying to delete the contents of the 686 // directory, so we shouldn't try to delete the directory as it will probably fail. 687 if (exceptions == null) { 688 Files.delete(path); 689 } 690 691 return exceptions; 692 } catch (IOException e) { 693 return addException(exceptions, e); 694 } 695 } 696 697 /** 698 * Simple, insecure method for deleting the contents of a directory for file systems that don't 699 * support {@code SecureDirectoryStream}. Returns a collection of exceptions that occurred or null 700 * if no exceptions were thrown. 701 */ 702 private static @Nullable Collection<IOException> deleteDirectoryContentsInsecure( 703 DirectoryStream<Path> dir) { 704 Collection<IOException> exceptions = null; 705 try { 706 for (Path entry : dir) { 707 exceptions = concat(exceptions, deleteRecursivelyInsecure(entry)); 708 } 709 710 return exceptions; 711 } catch (DirectoryIteratorException e) { 712 return addException(exceptions, e.getCause()); 713 } 714 } 715 716 /** 717 * Returns a path to the parent directory of the given path. If the path actually has a parent 718 * path, this is simple. Otherwise, we need to do some trickier things. Returns null if the path 719 * is a root or is the empty path. 720 */ 721 private static @Nullable Path getParentPath(Path path) { 722 Path parent = path.getParent(); 723 724 // Paths that have a parent: 725 if (parent != null) { 726 // "/foo" ("/") 727 // "foo/bar" ("foo") 728 // "C:\foo" ("C:\") 729 // "\foo" ("\" - current drive for process on Windows) 730 // "C:foo" ("C:" - working dir of drive C on Windows) 731 return parent; 732 } 733 734 // Paths that don't have a parent: 735 if (path.getNameCount() == 0) { 736 // "/", "C:\", "\" (no parent) 737 // "" (undefined, though typically parent of working dir) 738 // "C:" (parent of working dir of drive C on Windows) 739 // 740 // For working dir paths ("" and "C:"), return null because: 741 // A) it's not specified that "" is the path to the working directory. 742 // B) if we're getting this path for recursive delete, it's typically not possible to 743 // delete the working dir with a relative path anyway, so it's ok to fail. 744 // C) if we're getting it for opening a new SecureDirectoryStream, there's no need to get 745 // the parent path anyway since we can safely open a DirectoryStream to the path without 746 // worrying about a symlink. 747 return null; 748 } else { 749 // "foo" (working dir) 750 return path.getFileSystem().getPath("."); 751 } 752 } 753 754 /** Checks that the given options allow an insecure delete, throwing an exception if not. */ 755 private static void checkAllowsInsecure(Path path, RecursiveDeleteOption[] options) 756 throws InsecureRecursiveDeleteException { 757 if (!Arrays.asList(options).contains(RecursiveDeleteOption.ALLOW_INSECURE)) { 758 throw new InsecureRecursiveDeleteException(path.toString()); 759 } 760 } 761 762 /** 763 * Adds the given exception to the given collection, creating the collection if it's null. Returns 764 * the collection. 765 */ 766 private static Collection<IOException> addException( 767 @Nullable Collection<IOException> exceptions, IOException e) { 768 if (exceptions == null) { 769 exceptions = new ArrayList<>(); // don't need Set semantics 770 } 771 exceptions.add(e); 772 return exceptions; 773 } 774 775 /** 776 * Concatenates the contents of the two given collections of exceptions. If either collection is 777 * null, the other collection is returned. Otherwise, the elements of {@code other} are added to 778 * {@code exceptions} and {@code exceptions} is returned. 779 */ 780 private static @Nullable Collection<IOException> concat( 781 @Nullable Collection<IOException> exceptions, @Nullable Collection<IOException> other) { 782 if (exceptions == null) { 783 return other; 784 } else if (other != null) { 785 exceptions.addAll(other); 786 } 787 return exceptions; 788 } 789 790 /** 791 * Throws an exception indicating that one or more files couldn't be deleted when deleting {@code 792 * path} or its contents. 793 * 794 * <p>If there is only one exception in the collection, and it is a {@link NoSuchFileException} 795 * thrown because {@code path} itself didn't exist, then throws that exception. Otherwise, the 796 * thrown exception contains all the exceptions in the given collection as suppressed exceptions. 797 */ 798 private static void throwDeleteFailed(Path path, Collection<IOException> exceptions) 799 throws FileSystemException { 800 NoSuchFileException pathNotFound = pathNotFound(path, exceptions); 801 if (pathNotFound != null) { 802 throw pathNotFound; 803 } 804 // TODO(cgdecker): Should there be a custom exception type for this? 805 // Also, should we try to include the Path of each file we may have failed to delete rather 806 // than just the exceptions that occurred? 807 FileSystemException deleteFailed = 808 new FileSystemException( 809 path.toString(), 810 null, 811 "failed to delete one or more files; see suppressed exceptions for details"); 812 for (IOException e : exceptions) { 813 deleteFailed.addSuppressed(e); 814 } 815 throw deleteFailed; 816 } 817 818 private static @Nullable NoSuchFileException pathNotFound( 819 Path path, Collection<IOException> exceptions) { 820 if (exceptions.size() != 1) { 821 return null; 822 } 823 IOException exception = getOnlyElement(exceptions); 824 if (!(exception instanceof NoSuchFileException)) { 825 return null; 826 } 827 NoSuchFileException noSuchFileException = (NoSuchFileException) exception; 828 String exceptionFile = noSuchFileException.getFile(); 829 if (exceptionFile == null) { 830 /* 831 * It's not clear whether this happens in practice, especially with the filesystem 832 * implementations that are built into java.nio. 833 */ 834 return null; 835 } 836 Path parentPath = getParentPath(path); 837 if (parentPath == null) { 838 /* 839 * This is probably impossible: 840 * 841 * - In deleteRecursively, we require the path argument to have a parent. 842 * 843 * - In deleteDirectoryContents, the path argument may have no parent. Fortunately, all the 844 * *other* paths we process will be descendants of that. That leaves only the original path 845 * argument for us to consider. And the only place we call pathNotFound is from 846 * throwDeleteFailed, and the other place that we call throwDeleteFailed inside 847 * deleteDirectoryContents is when an exception is thrown during the recursive steps. Any 848 * failure during the initial lookup of the path argument itself is rethrown directly. So 849 * any exception that we're seeing here is from a descendant, which naturally has a parent. 850 * I think. 851 * 852 * Still, if this can happen somehow (a weird filesystem implementation that lets callers 853 * change its working directly concurrently with a call to deleteDirectoryContents?), it makes 854 * more sense for us to fall back to a generic FileSystemException (by returning null here) 855 * than to dereference parentPath and end up producing NullPointerException. 856 */ 857 return null; 858 } 859 // requireNonNull is safe because paths have file names when they have parents. 860 Path pathResolvedFromParent = parentPath.resolve(requireNonNull(path.getFileName())); 861 if (exceptionFile.equals(pathResolvedFromParent.toString())) { 862 return noSuchFileException; 863 } 864 return null; 865 } 866}