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