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