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