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