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