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