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