Coverage Summary for Class: MoreFiles (com.google.common.io)

Class Method, % Line, %
MoreFiles 0% (0/30) 0% (0/175)
MoreFiles$1 0% (0/2) 0% (0/2)
MoreFiles$2 0% (0/3) 0% (0/3)
MoreFiles$3 0% (0/3) 0% (0/3)
MoreFiles$PathByteSink 0% (0/4) 0% (0/6)
MoreFiles$PathByteSource 0% (0/11) 0% (0/33)
MoreFiles$PathByteSource$1 0% (0/2) 0% (0/2)
Total 0% (0/55) 0% (0/224)


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