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