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