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