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.common.io.ByteSource.AsCharSource;
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 21.0
067 * @author Colin Decker
068 */
069@Beta
070@GwtIncompatible
071@J2ObjCIncompatible // java.nio.file
072public final class MoreFiles {
073
074  private MoreFiles() {}
075
076  /**
077   * Returns a view of the given {@code path} as a {@link ByteSource}.
078   *
079   * <p>Any {@linkplain OpenOption open options} provided are used when opening streams to the file
080   * and may affect the behavior of the returned source and the streams it provides. See {@link
081   * StandardOpenOption} for the standard options that may be provided. Providing no options is
082   * equivalent to providing the {@link StandardOpenOption#READ READ} option.
083   */
084  public static ByteSource asByteSource(Path path, OpenOption... options) {
085    return new PathByteSource(path, options);
086  }
087
088  private static final class PathByteSource extends ByteSource {
089
090    private static final LinkOption[] FOLLOW_LINKS = {};
091
092    private final Path path;
093    private final OpenOption[] options;
094    private final boolean followLinks;
095
096    private PathByteSource(Path path, OpenOption... options) {
097      this.path = checkNotNull(path);
098      this.options = options.clone();
099      this.followLinks = followLinks(this.options);
100      // TODO(cgdecker): validate the provided options... for example, just WRITE seems wrong
101    }
102
103    private static boolean followLinks(OpenOption[] options) {
104      for (OpenOption option : options) {
105        if (option == NOFOLLOW_LINKS) {
106          return false;
107        }
108      }
109      return true;
110    }
111
112    @Override
113    public InputStream openStream() throws IOException {
114      return Files.newInputStream(path, options);
115    }
116
117    private BasicFileAttributes readAttributes() throws IOException {
118      return Files.readAttributes(
119          path,
120          BasicFileAttributes.class,
121          followLinks ? FOLLOW_LINKS : new LinkOption[] {NOFOLLOW_LINKS});
122    }
123
124    @Override
125    public Optional<Long> sizeIfKnown() {
126      BasicFileAttributes attrs;
127      try {
128        attrs = readAttributes();
129      } catch (IOException e) {
130        // Failed to get attributes; we don't know the size.
131        return Optional.absent();
132      }
133
134      // Don't return a size for directories or symbolic links; their sizes are implementation
135      // specific and they can't be read as bytes using the read methods anyway.
136      if (attrs.isDirectory() || attrs.isSymbolicLink()) {
137        return Optional.absent();
138      }
139
140      return Optional.of(attrs.size());
141    }
142
143    @Override
144    public long size() throws IOException {
145      BasicFileAttributes attrs = readAttributes();
146
147      // Don't return a size for directories or symbolic links; their sizes are implementation
148      // specific and they can't be read as bytes using the read methods anyway.
149      if (attrs.isDirectory()) {
150        throw new IOException("can't read: is a directory");
151      } else if (attrs.isSymbolicLink()) {
152        throw new IOException("can't read: is a symbolic link");
153      }
154
155      return attrs.size();
156    }
157
158    @Override
159    public byte[] read() throws IOException {
160      try (SeekableByteChannel channel = Files.newByteChannel(path, options)) {
161        return ByteStreams.toByteArray(Channels.newInputStream(channel), channel.size());
162      }
163    }
164
165    @Override
166    public CharSource asCharSource(Charset charset) {
167      if (options.length == 0) {
168        // If no OpenOptions were passed, delegate to Files.lines, which could have performance
169        // advantages. (If OpenOptions were passed we can't, because Files.lines doesn't have an
170        // overload taking OpenOptions, meaning we can't guarantee the same behavior w.r.t. things
171        // like following/not following symlinks.
172        return new AsCharSource(charset) {
173          @SuppressWarnings("FilesLinesLeak") // the user needs to close it in this case
174          @Override
175          public Stream<String> lines() throws IOException {
176            return Files.lines(path, charset);
177          }
178        };
179      }
180
181      return super.asCharSource(charset);
182    }
183
184    @Override
185    public String toString() {
186      return "MoreFiles.asByteSource(" + path + ", " + Arrays.toString(options) + ")";
187    }
188  }
189
190  /**
191   * Returns a view of the given {@code path} as a {@link ByteSink}.
192   *
193   * <p>Any {@linkplain OpenOption open options} provided are used when opening streams to the file
194   * and may affect the behavior of the returned sink and the streams it provides. See {@link
195   * StandardOpenOption} for the standard options that may be provided. Providing no options is
196   * equivalent to providing the {@link StandardOpenOption#CREATE CREATE}, {@link
197   * StandardOpenOption#TRUNCATE_EXISTING TRUNCATE_EXISTING} and {@link StandardOpenOption#WRITE
198   * WRITE} options.
199   */
200  public static ByteSink asByteSink(Path path, OpenOption... options) {
201    return new PathByteSink(path, options);
202  }
203
204  private static final class PathByteSink extends ByteSink {
205
206    private final Path path;
207    private final OpenOption[] options;
208
209    private PathByteSink(Path path, OpenOption... options) {
210      this.path = checkNotNull(path);
211      this.options = options.clone();
212      // TODO(cgdecker): validate the provided options... for example, just READ seems wrong
213    }
214
215    @Override
216    public OutputStream openStream() throws IOException {
217      return Files.newOutputStream(path, options);
218    }
219
220    @Override
221    public String toString() {
222      return "MoreFiles.asByteSink(" + path + ", " + Arrays.toString(options) + ")";
223    }
224  }
225
226  /**
227   * Returns a view of the given {@code path} as a {@link CharSource} using the given {@code
228   * charset}.
229   *
230   * <p>Any {@linkplain OpenOption open options} provided are used when opening streams to the file
231   * and may affect the behavior of the returned source and the streams it provides. See {@link
232   * StandardOpenOption} for the standard options that may be provided. Providing no options is
233   * equivalent to providing the {@link StandardOpenOption#READ READ} option.
234   */
235  public static CharSource asCharSource(Path path, Charset charset, OpenOption... options) {
236    return asByteSource(path, options).asCharSource(charset);
237  }
238
239  /**
240   * Returns a view of the given {@code path} as a {@link CharSink} using the given {@code charset}.
241   *
242   * <p>Any {@linkplain OpenOption open options} provided are used when opening streams to the file
243   * and may affect the behavior of the returned sink and the streams it provides. See {@link
244   * StandardOpenOption} for the standard options that may be provided. Providing no options is
245   * equivalent to providing the {@link StandardOpenOption#CREATE CREATE}, {@link
246   * StandardOpenOption#TRUNCATE_EXISTING TRUNCATE_EXISTING} and {@link StandardOpenOption#WRITE
247   * WRITE} options.
248   */
249  public static CharSink asCharSink(Path path, Charset charset, OpenOption... options) {
250    return asByteSink(path, options).asCharSink(charset);
251  }
252
253  /**
254   * Returns an immutable list of paths to the files contained in the given directory.
255   *
256   * @throws NoSuchFileException if the file does not exist <i>(optional specific exception)</i>
257   * @throws NotDirectoryException if the file could not be opened because it is not a directory
258   *     <i>(optional specific exception)</i>
259   * @throws IOException if an I/O error occurs
260   */
261  public static ImmutableList<Path> listFiles(Path dir) throws IOException {
262    try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
263      return ImmutableList.copyOf(stream);
264    } catch (DirectoryIteratorException e) {
265      throw e.getCause();
266    }
267  }
268
269  /**
270   * Returns a {@link Traverser} instance for the file and directory tree. The returned traverser
271   * starts from a {@link Path} and will return all files and directories it encounters.
272   *
273   * <p>The returned traverser attempts to avoid following symbolic links to directories. However,
274   * the traverser cannot guarantee that it will not follow symbolic links to directories as it is
275   * possible for a directory to be replaced with a symbolic link between checking if the file is a
276   * directory and actually reading the contents of that directory.
277   *
278   * <p>If the {@link Path} passed to one of the traversal methods does not exist or is not a
279   * directory, no exception will be thrown and the returned {@link Iterable} will contain a single
280   * element: that 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   * <p>Example: {@code MoreFiles.fileTraverser().breadthFirst("/")} may return files with the
287   * following paths: {@code ["/", "/etc", "/home", "/usr", "/etc/config.txt", "/etc/fonts", ...]}
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  public static void touch(Path path) throws IOException {
395    checkNotNull(path);
396
397    try {
398      Files.setLastModifiedTime(path, FileTime.fromMillis(System.currentTimeMillis()));
399    } catch (NoSuchFileException e) {
400      try {
401        Files.createFile(path);
402      } catch (FileAlreadyExistsException ignore) {
403        // The file didn't exist when we called setLastModifiedTime, but it did when we called
404        // createFile, so something else created the file in between. The end result is
405        // what we wanted: a new file that probably has its last modified time set to approximately
406        // now. Or it could have an arbitrary last modified time set by the creator, but that's no
407        // different than if another process set its last modified time to something else after we
408        // created it here.
409      }
410    }
411  }
412
413  /**
414   * Creates any necessary but nonexistent parent directories of the specified path. Note that if
415   * this operation fails, it may have succeeded in creating some (but not all) of the necessary
416   * parent directories. The parent directory is created with the given {@code attrs}.
417   *
418   * @throws IOException if an I/O error occurs, or if any necessary but nonexistent parent
419   *     directories of the specified file could not be created.
420   */
421  public static void createParentDirectories(Path path, FileAttribute<?>... attrs)
422      throws IOException {
423    // Interestingly, unlike File.getCanonicalFile(), Path/Files provides no way of getting the
424    // canonical (absolute, normalized, symlinks resolved, etc.) form of a path to a nonexistent
425    // file. getCanonicalFile() can at least get the canonical form of the part of the path which
426    // actually exists and then append the normalized remainder of the path to that.
427    Path normalizedAbsolutePath = path.toAbsolutePath().normalize();
428    Path parent = normalizedAbsolutePath.getParent();
429    if (parent == null) {
430      // The given directory is a filesystem root. All zero of its ancestors exist. This doesn't
431      // mean that the root itself exists -- consider x:\ on a Windows machine without such a
432      // drive -- or even that the caller can create it, but this method makes no such guarantees
433      // even for non-root files.
434      return;
435    }
436
437    // Check if the parent is a directory first because createDirectories will fail if the parent
438    // exists and is a symlink to a directory... we'd like for this to succeed in that case.
439    // (I'm kind of surprised that createDirectories would fail in that case; doesn't seem like
440    // what you'd want to happen.)
441    if (!Files.isDirectory(parent)) {
442      Files.createDirectories(parent, attrs);
443      if (!Files.isDirectory(parent)) {
444        throw new IOException("Unable to create parent directories of " + path);
445      }
446    }
447  }
448
449  /**
450   * Returns the <a href="http://en.wikipedia.org/wiki/Filename_extension">file extension</a> for
451   * the file at the given path, or the empty string if the file has no extension. The result does
452   * not include the '{@code .}'.
453   *
454   * <p><b>Note:</b> This method simply returns everything after the last '{@code .}' in the file's
455   * name as determined by {@link Path#getFileName}. It does not account for any filesystem-specific
456   * behavior that the {@link Path} API does not already account for. For example, on NTFS it will
457   * report {@code "txt"} as the extension for the filename {@code "foo.exe:.txt"} even though NTFS
458   * will drop the {@code ":.txt"} part of the name when the file is actually created on the
459   * filesystem due to NTFS's <a href="https://goo.gl/vTpJi4">Alternate Data Streams</a>.
460   */
461  public static String getFileExtension(Path path) {
462    Path name = path.getFileName();
463
464    // null for empty paths and root-only paths
465    if (name == null) {
466      return "";
467    }
468
469    String fileName = name.toString();
470    int dotIndex = fileName.lastIndexOf('.');
471    return dotIndex == -1 ? "" : fileName.substring(dotIndex + 1);
472  }
473
474  /**
475   * Returns the file name without its <a
476   * href="http://en.wikipedia.org/wiki/Filename_extension">file extension</a> or path. This is
477   * similar to the {@code basename} unix command. The result does not include the '{@code .}'.
478   */
479  public static String getNameWithoutExtension(Path path) {
480    Path name = path.getFileName();
481
482    // null for empty paths and root-only paths
483    if (name == null) {
484      return "";
485    }
486
487    String fileName = name.toString();
488    int dotIndex = fileName.lastIndexOf('.');
489    return dotIndex == -1 ? fileName : fileName.substring(0, dotIndex);
490  }
491
492  /**
493   * Deletes the file or directory at the given {@code path} recursively. Deletes symbolic links,
494   * not their targets (subject to the caveat below).
495   *
496   * <p>If an I/O exception occurs attempting to read, open or delete any file under the given
497   * directory, this method skips that file and continues. All such exceptions are collected and,
498   * after attempting to delete all files, an {@code IOException} is thrown containing those
499   * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}.
500   *
501   * <h2>Warning: Security of recursive deletes</h2>
502   *
503   * <p>On a file system that supports symbolic links and does <i>not</i> support {@link
504   * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories
505   * that are <i>outside</i> the directory being deleted. This can happen if, after checking that a
506   * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to
507   * an outside directory before the call that opens the directory to read its entries.
508   *
509   * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't
510   * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway,
511   * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior.
512   *
513   * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific exception)</i>
514   * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be
515   *     guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not
516   *     specified
517   * @throws IOException if {@code path} or any file in the subtree rooted at it can't be deleted
518   *     for any reason
519   */
520  public static void deleteRecursively(Path path, RecursiveDeleteOption... options)
521      throws IOException {
522    Path parentPath = getParentPath(path);
523    if (parentPath == null) {
524      throw new FileSystemException(path.toString(), null, "can't delete recursively");
525    }
526
527    Collection<IOException> exceptions = null; // created lazily if needed
528    try {
529      boolean sdsSupported = false;
530      try (DirectoryStream<Path> parent = Files.newDirectoryStream(parentPath)) {
531        if (parent instanceof SecureDirectoryStream) {
532          sdsSupported = true;
533          exceptions =
534              deleteRecursivelySecure((SecureDirectoryStream<Path>) parent, path.getFileName());
535        }
536      }
537
538      if (!sdsSupported) {
539        checkAllowsInsecure(path, options);
540        exceptions = deleteRecursivelyInsecure(path);
541      }
542    } catch (IOException e) {
543      if (exceptions == null) {
544        throw e;
545      } else {
546        exceptions.add(e);
547      }
548    }
549
550    if (exceptions != null) {
551      throwDeleteFailed(path, exceptions);
552    }
553  }
554
555  /**
556   * Deletes all files within the directory at the given {@code path} {@linkplain #deleteRecursively
557   * recursively}. Does not delete the directory itself. Deletes symbolic links, not their targets
558   * (subject to the caveat below). If {@code path} itself is a symbolic link to a directory, that
559   * link is followed and the contents of the directory it targets are deleted.
560   *
561   * <p>If an I/O exception occurs attempting to read, open or delete any file under the given
562   * directory, this method skips that file and continues. All such exceptions are collected and,
563   * after attempting to delete all files, an {@code IOException} is thrown containing those
564   * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}.
565   *
566   * <h2>Warning: Security of recursive deletes</h2>
567   *
568   * <p>On a file system that supports symbolic links and does <i>not</i> support {@link
569   * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories
570   * that are <i>outside</i> the directory being deleted. This can happen if, after checking that a
571   * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to
572   * an outside directory before the call that opens the directory to read its entries.
573   *
574   * <p>By default, this method throws {@link InsecureRecursiveDeleteException} if it can't
575   * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway,
576   * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior.
577   *
578   * @throws NoSuchFileException if {@code path} does not exist <i>(optional specific exception)</i>
579   * @throws NotDirectoryException if the file at {@code path} is not a directory <i>(optional
580   *     specific exception)</i>
581   * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be
582   *     guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not
583   *     specified
584   * @throws IOException if one or more files can't be deleted for any reason
585   */
586  public static void deleteDirectoryContents(Path path, RecursiveDeleteOption... options)
587      throws IOException {
588    Collection<IOException> exceptions = null; // created lazily if needed
589    try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
590      if (stream instanceof SecureDirectoryStream) {
591        SecureDirectoryStream<Path> sds = (SecureDirectoryStream<Path>) stream;
592        exceptions = deleteDirectoryContentsSecure(sds);
593      } else {
594        checkAllowsInsecure(path, options);
595        exceptions = deleteDirectoryContentsInsecure(stream);
596      }
597    } catch (IOException e) {
598      if (exceptions == null) {
599        throw e;
600      } else {
601        exceptions.add(e);
602      }
603    }
604
605    if (exceptions != null) {
606      throwDeleteFailed(path, exceptions);
607    }
608  }
609
610  /**
611   * Secure recursive delete using {@code SecureDirectoryStream}. Returns a collection of exceptions
612   * that occurred or null if no exceptions were thrown.
613   */
614  private static @Nullable Collection<IOException> deleteRecursivelySecure(
615      SecureDirectoryStream<Path> dir, Path path) {
616    Collection<IOException> exceptions = null;
617    try {
618      if (isDirectory(dir, path, NOFOLLOW_LINKS)) {
619        try (SecureDirectoryStream<Path> childDir = dir.newDirectoryStream(path, NOFOLLOW_LINKS)) {
620          exceptions = deleteDirectoryContentsSecure(childDir);
621        }
622
623        // If exceptions is not null, something went wrong trying to delete the contents of the
624        // directory, so we shouldn't try to delete the directory as it will probably fail.
625        if (exceptions == null) {
626          dir.deleteDirectory(path);
627        }
628      } else {
629        dir.deleteFile(path);
630      }
631
632      return exceptions;
633    } catch (IOException e) {
634      return addException(exceptions, e);
635    }
636  }
637
638  /**
639   * Secure method for deleting the contents of a directory using {@code SecureDirectoryStream}.
640   * Returns a collection of exceptions that occurred or null if no exceptions were thrown.
641   */
642  private static @Nullable Collection<IOException> deleteDirectoryContentsSecure(
643      SecureDirectoryStream<Path> dir) {
644    Collection<IOException> exceptions = null;
645    try {
646      for (Path path : dir) {
647        exceptions = concat(exceptions, deleteRecursivelySecure(dir, path.getFileName()));
648      }
649
650      return exceptions;
651    } catch (DirectoryIteratorException e) {
652      return addException(exceptions, e.getCause());
653    }
654  }
655
656  /**
657   * Insecure recursive delete for file systems that don't support {@code SecureDirectoryStream}.
658   * Returns a collection of exceptions that occurred or null if no exceptions were thrown.
659   */
660  private static @Nullable Collection<IOException> deleteRecursivelyInsecure(Path path) {
661    Collection<IOException> exceptions = null;
662    try {
663      if (Files.isDirectory(path, NOFOLLOW_LINKS)) {
664        try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
665          exceptions = deleteDirectoryContentsInsecure(stream);
666        }
667      }
668
669      // If exceptions is not null, something went wrong trying to delete the contents of the
670      // directory, so we shouldn't try to delete the directory as it will probably fail.
671      if (exceptions == null) {
672        Files.delete(path);
673      }
674
675      return exceptions;
676    } catch (IOException e) {
677      return addException(exceptions, e);
678    }
679  }
680
681  /**
682   * Simple, insecure method for deleting the contents of a directory for file systems that don't
683   * support {@code SecureDirectoryStream}. Returns a collection of exceptions that occurred or null
684   * if no exceptions were thrown.
685   */
686  private static @Nullable Collection<IOException> deleteDirectoryContentsInsecure(
687      DirectoryStream<Path> dir) {
688    Collection<IOException> exceptions = null;
689    try {
690      for (Path entry : dir) {
691        exceptions = concat(exceptions, deleteRecursivelyInsecure(entry));
692      }
693
694      return exceptions;
695    } catch (DirectoryIteratorException e) {
696      return addException(exceptions, e.getCause());
697    }
698  }
699
700  /**
701   * Returns a path to the parent directory of the given path. If the path actually has a parent
702   * path, this is simple. Otherwise, we need to do some trickier things. Returns null if the path
703   * is a root or is the empty path.
704   */
705  private static @Nullable Path getParentPath(Path path) {
706    Path parent = path.getParent();
707
708    // Paths that have a parent:
709    if (parent != null) {
710      // "/foo" ("/")
711      // "foo/bar" ("foo")
712      // "C:\foo" ("C:\")
713      // "\foo" ("\" - current drive for process on Windows)
714      // "C:foo" ("C:" - working dir of drive C on Windows)
715      return parent;
716    }
717
718    // Paths that don't have a parent:
719    if (path.getNameCount() == 0) {
720      // "/", "C:\", "\" (no parent)
721      // "" (undefined, though typically parent of working dir)
722      // "C:" (parent of working dir of drive C on Windows)
723      //
724      // For working dir paths ("" and "C:"), return null because:
725      //   A) it's not specified that "" is the path to the working directory.
726      //   B) if we're getting this path for recursive delete, it's typically not possible to
727      //      delete the working dir with a relative path anyway, so it's ok to fail.
728      //   C) if we're getting it for opening a new SecureDirectoryStream, there's no need to get
729      //      the parent path anyway since we can safely open a DirectoryStream to the path without
730      //      worrying about a symlink.
731      return null;
732    } else {
733      // "foo" (working dir)
734      return path.getFileSystem().getPath(".");
735    }
736  }
737
738  /** Checks that the given options allow an insecure delete, throwing an exception if not. */
739  private static void checkAllowsInsecure(Path path, RecursiveDeleteOption[] options)
740      throws InsecureRecursiveDeleteException {
741    if (!Arrays.asList(options).contains(RecursiveDeleteOption.ALLOW_INSECURE)) {
742      throw new InsecureRecursiveDeleteException(path.toString());
743    }
744  }
745
746  /**
747   * Adds the given exception to the given collection, creating the collection if it's null. Returns
748   * the collection.
749   */
750  private static Collection<IOException> addException(
751      @Nullable Collection<IOException> exceptions, IOException e) {
752    if (exceptions == null) {
753      exceptions = new ArrayList<>(); // don't need Set semantics
754    }
755    exceptions.add(e);
756    return exceptions;
757  }
758
759  /**
760   * Concatenates the contents of the two given collections of exceptions. If either collection is
761   * null, the other collection is returned. Otherwise, the elements of {@code other} are added to
762   * {@code exceptions} and {@code exceptions} is returned.
763   */
764  private static @Nullable Collection<IOException> concat(
765      @Nullable Collection<IOException> exceptions, @Nullable Collection<IOException> other) {
766    if (exceptions == null) {
767      return other;
768    } else if (other != null) {
769      exceptions.addAll(other);
770    }
771    return exceptions;
772  }
773
774  /**
775   * Throws an exception indicating that one or more files couldn't be deleted. The thrown exception
776   * contains all the exceptions in the given collection as suppressed exceptions.
777   */
778  private static void throwDeleteFailed(Path path, Collection<IOException> exceptions)
779      throws FileSystemException {
780    // TODO(cgdecker): Should there be a custom exception type for this?
781    // Also, should we try to include the Path of each file we may have failed to delete rather
782    // than just the exceptions that occurred?
783    FileSystemException deleteFailed =
784        new FileSystemException(
785            path.toString(),
786            null,
787            "failed to delete one or more files; see suppressed exceptions for details");
788    for (IOException e : exceptions) {
789      deleteFailed.addSuppressed(e);
790    }
791    throw deleteFailed;
792  }
793}