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