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