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