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