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.graph.SuccessorsFunction;
029import com.google.common.graph.Traverser;
030import com.google.common.io.ByteSource.AsCharSource;
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.checkerframework.checker.nullness.compatqual.NullableDecl;
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
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 ByteSource {
090
091    private static final LinkOption[] FOLLOW_LINKS = {};
092
093    private final Path path;
094    private final OpenOption[] options;
095    private final boolean followLinks;
096
097    private PathByteSource(Path path, OpenOption... options) {
098      this.path = checkNotNull(path);
099      this.options = options.clone();
100      this.followLinks = followLinks(this.options);
101      // TODO(cgdecker): validate the provided options... for example, just WRITE seems wrong
102    }
103
104    private static boolean followLinks(OpenOption[] options) {
105      for (OpenOption option : options) {
106        if (option == NOFOLLOW_LINKS) {
107          return false;
108        }
109      }
110      return true;
111    }
112
113    @Override
114    public InputStream openStream() throws IOException {
115      return Files.newInputStream(path, options);
116    }
117
118    private BasicFileAttributes readAttributes() throws IOException {
119      return Files.readAttributes(
120          path,
121          BasicFileAttributes.class,
122          followLinks ? FOLLOW_LINKS : new LinkOption[] {NOFOLLOW_LINKS});
123    }
124
125    @Override
126    public Optional<Long> sizeIfKnown() {
127      BasicFileAttributes attrs;
128      try {
129        attrs = readAttributes();
130      } catch (IOException e) {
131        // Failed to get attributes; we don't know the size.
132        return Optional.absent();
133      }
134
135      // Don't return a size for directories or symbolic links; their sizes are implementation
136      // specific and they can't be read as bytes using the read methods anyway.
137      if (attrs.isDirectory() || attrs.isSymbolicLink()) {
138        return Optional.absent();
139      }
140
141      return Optional.of(attrs.size());
142    }
143
144    @Override
145    public long size() throws IOException {
146      BasicFileAttributes attrs = readAttributes();
147
148      // Don't return a size for directories or symbolic links; their sizes are implementation
149      // specific and they can't be read as bytes using the read methods anyway.
150      if (attrs.isDirectory()) {
151        throw new IOException("can't read: is a directory");
152      } else if (attrs.isSymbolicLink()) {
153        throw new IOException("can't read: is a symbolic link");
154      }
155
156      return attrs.size();
157    }
158
159    @Override
160    public byte[] read() throws IOException {
161      try (SeekableByteChannel channel = Files.newByteChannel(path, options)) {
162        return com.google.common.io.Files.readFile(
163            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 TreeTraverser} for traversing a directory tree. The returned traverser
273   * attempts to avoid following symbolic links to directories. However, the traverser cannot
274   * guarantee that it will not follow symbolic links to directories as it is possible for a
275   * directory to be replaced with a symbolic link between checking if the file is a directory and
276   * actually reading the contents of that directory.
277   *
278   * <p>Note that if the {@link Path} passed to one of the traversal methods does not exist, no
279   * exception will be thrown and the returned {@link Iterable} will contain a single element: that
280   * path.
281   *
282   * <p>{@link DirectoryIteratorException} may be thrown when iterating {@link Iterable} instances
283   * created by this traverser if an {@link IOException} is thrown by a call to {@link
284   * #listFiles(Path)}.
285   *
286   * @deprecated The returned {@link TreeTraverser} type is deprecated. Use the replacement method
287   *     {@link #fileTraverser()} instead with the same semantics as this method. This method is
288   *     scheduled to be removed in April 2018.
289   */
290  @Deprecated
291  public static TreeTraverser<Path> directoryTreeTraverser() {
292    return DirectoryTreeTraverser.INSTANCE;
293  }
294
295  private static final class DirectoryTreeTraverser extends TreeTraverser<Path> {
296
297    private static final DirectoryTreeTraverser INSTANCE = new DirectoryTreeTraverser();
298
299    @Override
300    public Iterable<Path> children(Path dir) {
301      return fileTreeChildren(dir);
302    }
303  }
304
305  /**
306   * Returns a {@link Traverser} instance for the file and directory tree. The returned traverser
307   * starts from a {@link Path} and will return all files and directories it encounters.
308   *
309   * <p>The returned traverser attempts to avoid following symbolic links to directories. However,
310   * the traverser cannot guarantee that it will not follow symbolic links to directories as it is
311   * possible for a directory to be replaced with a symbolic link between checking if the file is a
312   * directory and actually reading the contents of that directory.
313   *
314   * <p>If the {@link Path} passed to one of the traversal methods does not exist or is not a
315   * directory, no exception will be thrown and the returned {@link Iterable} will contain a single
316   * element: that path.
317   *
318   * <p>{@link DirectoryIteratorException} may be thrown when iterating {@link Iterable} instances
319   * created by this traverser if an {@link IOException} is thrown by a call to {@link
320   * #listFiles(Path)}.
321   *
322   * <p>Example: {@code MoreFiles.fileTraverser().breadthFirst("/")} may return files with the
323   * following paths: {@code ["/", "/etc", "/home", "/usr", "/etc/config.txt", "/etc/fonts", ...]}
324   *
325   * @since 23.5
326   */
327  public static Traverser<Path> fileTraverser() {
328    return Traverser.forTree(FILE_TREE);
329  }
330
331  private static final SuccessorsFunction<Path> FILE_TREE =
332      new SuccessorsFunction<Path>() {
333        @Override
334        public Iterable<Path> successors(Path path) {
335          return fileTreeChildren(path);
336        }
337      };
338
339  private static Iterable<Path> fileTreeChildren(Path dir) {
340    if (Files.isDirectory(dir, NOFOLLOW_LINKS)) {
341      try {
342        return listFiles(dir);
343      } catch (IOException e) {
344        // the exception thrown when iterating a DirectoryStream if an I/O exception occurs
345        throw new DirectoryIteratorException(e);
346      }
347    }
348    return ImmutableList.of();
349  }
350
351  /**
352   * Returns a predicate that returns the result of {@link java.nio.file.Files#isDirectory(Path,
353   * LinkOption...)} on input paths with the given link options.
354   */
355  public static Predicate<Path> isDirectory(LinkOption... options) {
356    final LinkOption[] optionsCopy = options.clone();
357    return new Predicate<Path>() {
358      @Override
359      public boolean apply(Path input) {
360        return Files.isDirectory(input, optionsCopy);
361      }
362
363      @Override
364      public String toString() {
365        return "MoreFiles.isDirectory(" + Arrays.toString(optionsCopy) + ")";
366      }
367    };
368  }
369
370  /** Returns whether or not the file with the given name in the given dir is a directory. */
371  private static boolean isDirectory(
372      SecureDirectoryStream<Path> dir, Path name, LinkOption... options) throws IOException {
373    return dir.getFileAttributeView(name, BasicFileAttributeView.class, options)
374        .readAttributes()
375        .isDirectory();
376  }
377
378  /**
379   * Returns a predicate that returns the result of {@link java.nio.file.Files#isRegularFile(Path,
380   * LinkOption...)} on input paths with the given link options.
381   */
382  public static Predicate<Path> isRegularFile(LinkOption... options) {
383    final LinkOption[] optionsCopy = options.clone();
384    return new Predicate<Path>() {
385      @Override
386      public boolean apply(Path input) {
387        return Files.isRegularFile(input, optionsCopy);
388      }
389
390      @Override
391      public String toString() {
392        return "MoreFiles.isRegularFile(" + Arrays.toString(optionsCopy) + ")";
393      }
394    };
395  }
396
397  /**
398   * Returns true if the files located by the given paths exist, are not directories, and contain
399   * the same bytes.
400   *
401   * @throws IOException if an I/O error occurs
402   * @since 22.0
403   */
404  public static boolean equal(Path path1, Path path2) throws IOException {
405    checkNotNull(path1);
406    checkNotNull(path2);
407    if (Files.isSameFile(path1, path2)) {
408      return true;
409    }
410
411    /*
412     * Some operating systems may return zero as the length for files denoting system-dependent
413     * entities such as devices or pipes, in which case we must fall back on comparing the bytes
414     * directly.
415     */
416    ByteSource source1 = asByteSource(path1);
417    ByteSource source2 = asByteSource(path2);
418    long len1 = source1.sizeIfKnown().or(0L);
419    long len2 = source2.sizeIfKnown().or(0L);
420    if (len1 != 0 && len2 != 0 && len1 != len2) {
421      return false;
422    }
423    return source1.contentEquals(source2);
424  }
425
426  /**
427   * Like the unix command of the same name, creates an empty file or updates the last modified
428   * timestamp of the existing file at the given path to the current system time.
429   */
430  public static void touch(Path path) throws IOException {
431    checkNotNull(path);
432
433    try {
434      Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis()));
435    } catch (NoSuchFileException e) {
436      try {
437        Files.createFile(path);
438      } catch (FileAlreadyExistsException ignore) {
439        // The file didn't exist when we called setLastModifiedTime, but it did when we called
440        // createFile, so something else created the file in between. The end result is
441        // what we wanted: a new file that probably has its last modified time set to approximately
442        // now. Or it could have an arbitrary last modified time set by the creator, but that's no
443        // different than if another process set its last modified time to something else after we
444        // created it here.
445      }
446    }
447  }
448
449  /**
450   * Creates any necessary but nonexistent parent directories of the specified path. Note that if
451   * this operation fails, it may have succeeded in creating some (but not all) of the necessary
452   * parent directories. The parent directory is created with the given {@code attrs}.
453   *
454   * @throws IOException if an I/O error occurs, or if any necessary but nonexistent parent
455   *     directories of the specified file could not be created.
456   */
457  public static void createParentDirectories(Path path, FileAttribute<?>... attrs)
458      throws IOException {
459    // Interestingly, unlike File.getCanonicalFile(), Path/Files provides no way of getting the
460    // canonical (absolute, normalized, symlinks resolved, etc.) form of a path to a nonexistent
461    // file. getCanonicalFile() can at least get the canonical form of the part of the path which
462    // actually exists and then append the normalized remainder of the path to that.
463    Path normalizedAbsolutePath = path.toAbsolutePath().normalize();
464    Path parent = normalizedAbsolutePath.getParent();
465    if (parent == null) {
466      // The given directory is a filesystem root. All zero of its ancestors exist. This doesn't
467      // mean that the root itself exists -- consider x:\ on a Windows machine without such a
468      // drive -- or even that the caller can create it, but this method makes no such guarantees
469      // even for non-root files.
470      return;
471    }
472
473    // Check if the parent is a directory first because createDirectories will fail if the parent
474    // exists and is a symlink to a directory... we'd like for this to succeed in that case.
475    // (I'm kind of surprised that createDirectories would fail in that case; doesn't seem like
476    // what you'd want to happen.)
477    if (!Files.isDirectory(parent)) {
478      Files.createDirectories(parent, attrs);
479      if (!Files.isDirectory(parent)) {
480        throw new IOException("Unable to create parent directories of " + path);
481      }
482    }
483  }
484
485  /**
486   * Returns the <a href="http://en.wikipedia.org/wiki/Filename_extension">file extension</a> for
487   * the file at the given path, or the empty string if the file has no extension. The result does
488   * not include the '{@code .}'.
489   *
490   * <p><b>Note:</b> This method simply returns everything after the last '{@code .}' in the file's
491   * name as determined by {@link Path#getFileName}. It does not account for any filesystem-specific
492   * behavior that the {@link Path} API does not already account for. For example, on NTFS it will
493   * report {@code "txt"} as the extension for the filename {@code "foo.exe:.txt"} even though NTFS
494   * will drop the {@code ":.txt"} part of the name when the file is actually created on the
495   * filesystem due to NTFS's <a href="https://goo.gl/vTpJi4">Alternate Data Streams</a>.
496   */
497  public static String getFileExtension(Path path) {
498    Path name = path.getFileName();
499
500    // null for empty paths and root-only paths
501    if (name == null) {
502      return "";
503    }
504
505    String fileName = name.toString();
506    int dotIndex = fileName.lastIndexOf('.');
507    return dotIndex == -1 ? "" : fileName.substring(dotIndex + 1);
508  }
509
510  /**
511   * Returns the file name without its <a
512   * href="http://en.wikipedia.org/wiki/Filename_extension">file extension</a> or path. This is
513   * similar to the {@code basename} unix command. The result does not include the '{@code .}'.
514   */
515  public static String getNameWithoutExtension(Path path) {
516    Path name = path.getFileName();
517
518    // null for empty paths and root-only paths
519    if (name == null) {
520      return "";
521    }
522
523    String fileName = name.toString();
524    int dotIndex = fileName.lastIndexOf('.');
525    return dotIndex == -1 ? fileName : fileName.substring(0, dotIndex);
526  }
527
528  /**
529   * Deletes the file or directory at the given {@code path} recursively. Deletes symbolic links,
530   * not their targets (subject to the caveat below).
531   *
532   * <p>If an I/O exception occurs attempting to read, open or delete any file under the given
533   * directory, this method skips that file and continues. All such exceptions are collected and,
534   * after attempting to delete all files, an {@code IOException} is thrown containing those
535   * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}.
536   *
537   * <h2>Warning: Security of recursive deletes</h2>
538   *
539   * <p>On a file system that supports symbolic links and does <i>not</i> support {@link
540   * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories
541   * that are <i>outside</i> the directory being deleted. This can happen if, after checking that a
542   * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to
543   * an outside directory before the call that opens the directory to read its entries.
544   *
545   * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't
546   * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway,
547   * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior.
548   *
549   * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific exception)</i>
550   * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be
551   *     guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not
552   *     specified
553   * @throws IOException if {@code path} or any file in the subtree rooted at it can't be deleted
554   *     for any reason
555   */
556  public static void deleteRecursively(Path path, RecursiveDeleteOption... options)
557      throws IOException {
558    Path parentPath = getParentPath(path);
559    if (parentPath == null) {
560      throw new FileSystemException(path.toString(), null, "can't delete recursively");
561    }
562
563    Collection<IOException> exceptions = null; // created lazily if needed
564    try {
565      boolean sdsSupported = false;
566      try (DirectoryStream<Path> parent = Files.newDirectoryStream(parentPath)) {
567        if (parent instanceof SecureDirectoryStream) {
568          sdsSupported = true;
569          exceptions =
570              deleteRecursivelySecure((SecureDirectoryStream<Path>) parent, path.getFileName());
571        }
572      }
573
574      if (!sdsSupported) {
575        checkAllowsInsecure(path, options);
576        exceptions = deleteRecursivelyInsecure(path);
577      }
578    } catch (IOException e) {
579      if (exceptions == null) {
580        throw e;
581      } else {
582        exceptions.add(e);
583      }
584    }
585
586    if (exceptions != null) {
587      throwDeleteFailed(path, exceptions);
588    }
589  }
590
591  /**
592   * Deletes all files within the directory at the given {@code path} {@linkplain #deleteRecursively
593   * recursively}. Does not delete the directory itself. Deletes symbolic links, not their targets
594   * (subject to the caveat below). If {@code path} itself is a symbolic link to a directory, that
595   * link is followed and the contents of the directory it targets are deleted.
596   *
597   * <p>If an I/O exception occurs attempting to read, open or delete any file under the given
598   * directory, this method skips that file and continues. All such exceptions are collected and,
599   * after attempting to delete all files, an {@code IOException} is thrown containing those
600   * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}.
601   *
602   * <h2>Warning: Security of recursive deletes</h2>
603   *
604   * <p>On a file system that supports symbolic links and does <i>not</i> support {@link
605   * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories
606   * that are <i>outside</i> the directory being deleted. This can happen if, after checking that a
607   * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to
608   * an outside directory before the call that opens the directory to read its entries.
609   *
610   * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't
611   * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway,
612   * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior.
613   *
614   * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific exception)</i>
615   * @throws NotDirectoryException if the file at {@code path} is not a directory <i>(optional
616   *     specific exception)</i>
617   * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be
618   *     guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not
619   *     specified
620   * @throws IOException if one or more files can't be deleted for any reason
621   */
622  public static void deleteDirectoryContents(Path path, RecursiveDeleteOption... options)
623      throws IOException {
624    Collection<IOException> exceptions = null; // created lazily if needed
625    try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
626      if (stream instanceof SecureDirectoryStream) {
627        SecureDirectoryStream<Path> sds = (SecureDirectoryStream<Path>) stream;
628        exceptions = deleteDirectoryContentsSecure(sds);
629      } else {
630        checkAllowsInsecure(path, options);
631        exceptions = deleteDirectoryContentsInsecure(stream);
632      }
633    } catch (IOException e) {
634      if (exceptions == null) {
635        throw e;
636      } else {
637        exceptions.add(e);
638      }
639    }
640
641    if (exceptions != null) {
642      throwDeleteFailed(path, exceptions);
643    }
644  }
645
646  /**
647   * Secure recursive delete using {@code SecureDirectoryStream}. Returns a collection of exceptions
648   * that occurred or null if no exceptions were thrown.
649   */
650  @NullableDecl
651  private static Collection<IOException> deleteRecursivelySecure(
652      SecureDirectoryStream<Path> dir, Path path) {
653    Collection<IOException> exceptions = null;
654    try {
655      if (isDirectory(dir, path, NOFOLLOW_LINKS)) {
656        try (SecureDirectoryStream<Path> childDir = dir.newDirectoryStream(path, NOFOLLOW_LINKS)) {
657          exceptions = deleteDirectoryContentsSecure(childDir);
658        }
659
660        // If exceptions is not null, something went wrong trying to delete the contents of the
661        // directory, so we shouldn't try to delete the directory as it will probably fail.
662        if (exceptions == null) {
663          dir.deleteDirectory(path);
664        }
665      } else {
666        dir.deleteFile(path);
667      }
668
669      return exceptions;
670    } catch (IOException e) {
671      return addException(exceptions, e);
672    }
673  }
674
675  /**
676   * Secure method for deleting the contents of a directory using {@code SecureDirectoryStream}.
677   * Returns a collection of exceptions that occurred or null if no exceptions were thrown.
678   */
679  @NullableDecl
680  private static Collection<IOException> deleteDirectoryContentsSecure(
681      SecureDirectoryStream<Path> dir) {
682    Collection<IOException> exceptions = null;
683    try {
684      for (Path path : dir) {
685        exceptions = concat(exceptions, deleteRecursivelySecure(dir, path.getFileName()));
686      }
687
688      return exceptions;
689    } catch (DirectoryIteratorException e) {
690      return addException(exceptions, e.getCause());
691    }
692  }
693
694  /**
695   * Insecure recursive delete for file systems that don't support {@code SecureDirectoryStream}.
696   * Returns a collection of exceptions that occurred or null if no exceptions were thrown.
697   */
698  @NullableDecl
699  private static Collection<IOException> deleteRecursivelyInsecure(Path path) {
700    Collection<IOException> exceptions = null;
701    try {
702      if (Files.isDirectory(path, NOFOLLOW_LINKS)) {
703        try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
704          exceptions = deleteDirectoryContentsInsecure(stream);
705        }
706      }
707
708      // If exceptions is not null, something went wrong trying to delete the contents of the
709      // directory, so we shouldn't try to delete the directory as it will probably fail.
710      if (exceptions == null) {
711        Files.delete(path);
712      }
713
714      return exceptions;
715    } catch (IOException e) {
716      return addException(exceptions, e);
717    }
718  }
719
720  /**
721   * Simple, insecure method for deleting the contents of a directory for file systems that don't
722   * support {@code SecureDirectoryStream}. Returns a collection of exceptions that occurred or null
723   * if no exceptions were thrown.
724   */
725  @NullableDecl
726  private static Collection<IOException> deleteDirectoryContentsInsecure(
727      DirectoryStream<Path> dir) {
728    Collection<IOException> exceptions = null;
729    try {
730      for (Path entry : dir) {
731        exceptions = concat(exceptions, deleteRecursivelyInsecure(entry));
732      }
733
734      return exceptions;
735    } catch (DirectoryIteratorException e) {
736      return addException(exceptions, e.getCause());
737    }
738  }
739
740  /**
741   * Returns a path to the parent directory of the given path. If the path actually has a parent
742   * path, this is simple. Otherwise, we need to do some trickier things. Returns null if the path
743   * is a root or is the empty path.
744   */
745  @NullableDecl
746  private static Path getParentPath(Path path) {
747    Path parent = path.getParent();
748
749    // Paths that have a parent:
750    if (parent != null) {
751      // "/foo" ("/")
752      // "foo/bar" ("foo")
753      // "C:\foo" ("C:\")
754      // "\foo" ("\" - current drive for process on Windows)
755      // "C:foo" ("C:" - working dir of drive C on Windows)
756      return parent;
757    }
758
759    // Paths that don't have a parent:
760    if (path.getNameCount() == 0) {
761      // "/", "C:\", "\" (no parent)
762      // "" (undefined, though typically parent of working dir)
763      // "C:" (parent of working dir of drive C on Windows)
764      //
765      // For working dir paths ("" and "C:"), return null because:
766      //   A) it's not specified that "" is the path to the working directory.
767      //   B) if we're getting this path for recursive delete, it's typically not possible to
768      //      delete the working dir with a relative path anyway, so it's ok to fail.
769      //   C) if we're getting it for opening a new SecureDirectoryStream, there's no need to get
770      //      the parent path anyway since we can safely open a DirectoryStream to the path without
771      //      worrying about a symlink.
772      return null;
773    } else {
774      // "foo" (working dir)
775      return path.getFileSystem().getPath(".");
776    }
777  }
778
779  /** Checks that the given options allow an insecure delete, throwing an exception if not. */
780  private static void checkAllowsInsecure(Path path, RecursiveDeleteOption[] options)
781      throws InsecureRecursiveDeleteException {
782    if (!Arrays.asList(options).contains(RecursiveDeleteOption.ALLOW_INSECURE)) {
783      throw new InsecureRecursiveDeleteException(path.toString());
784    }
785  }
786
787  /**
788   * Adds the given exception to the given collection, creating the collection if it's null. Returns
789   * the collection.
790   */
791  private static Collection<IOException> addException(
792      @NullableDecl Collection<IOException> exceptions, IOException e) {
793    if (exceptions == null) {
794      exceptions = new ArrayList<>(); // don't need Set semantics
795    }
796    exceptions.add(e);
797    return exceptions;
798  }
799
800  /**
801   * Concatenates the contents of the two given collections of exceptions. If either collection is
802   * null, the other collection is returned. Otherwise, the elements of {@code other} are added to
803   * {@code exceptions} and {@code exceptions} is returned.
804   */
805  @NullableDecl
806  private static Collection<IOException> concat(
807      @NullableDecl Collection<IOException> exceptions,
808      @NullableDecl Collection<IOException> other) {
809    if (exceptions == null) {
810      return other;
811    } else if (other != null) {
812      exceptions.addAll(other);
813    }
814    return exceptions;
815  }
816
817  /**
818   * Throws an exception indicating that one or more files couldn't be deleted. The thrown exception
819   * contains all the exceptions in the given collection as suppressed exceptions.
820   */
821  private static void throwDeleteFailed(Path path, Collection<IOException> exceptions)
822      throws FileSystemException {
823    // TODO(cgdecker): Should there be a custom exception type for this?
824    // Also, should we try to include the Path of each file we may have failed to delete rather
825    // than just the exceptions that occurred?
826    FileSystemException deleteFailed =
827        new FileSystemException(
828            path.toString(),
829            null,
830            "failed to delete one or more files; see suppressed exceptions for details");
831    for (IOException e : exceptions) {
832      deleteFailed.addSuppressed(e);
833    }
834    throw deleteFailed;
835  }
836}