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