/** * Copyright 2015 Palantir Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.palantir.giraffe.file; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import java.io.IOException; import java.io.Writer; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystem; import java.nio.file.FileSystemException; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.spi.FileSystemProvider; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Set; import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.palantir.giraffe.file.base.attribute.PermissionChange; import com.palantir.giraffe.file.base.feature.LargeFileCopy; import com.palantir.giraffe.file.base.feature.RecursiveCopy; import com.palantir.giraffe.file.base.feature.RecursiveDelete; import com.palantir.giraffe.file.base.feature.RecursivePermissions; /** * Provides static methods that extend the functionality provided by * {@link Files}. * * @author bkeyes */ public final class MoreFiles { private static final DirectoryStream.Filter<Path> FILE_FILTER = new DirectoryStream.Filter<Path>() { @Override public boolean accept(Path entry) throws IOException { return Files.isRegularFile(entry); } }; private static final DirectoryStream.Filter<Path> DIRECTORY_FILTER = new DirectoryStream.Filter<Path>() { @Override public boolean accept(Path entry) throws IOException { return Files.isDirectory(entry); } }; /** * Returns a directory filter that matches {@linkplain Files#isRegularFile * regular} files. */ public static DirectoryStream.Filter<Path> regularFileFilter() { return FILE_FILTER; } /** * Returns a directory filter that matches {@linkplain Files#isDirectory * directories}. */ public static DirectoryStream.Filter<Path> directoryFilter() { return DIRECTORY_FILTER; } /** * Deletes a path recursively. * <p> * If the path is a file or an empty directory, this method is equivalent to * {@link Files#delete(Path)}. If the path is a non-empty directory, the * directory, all sub-directories, and all child files are deleted. * <p> * This operation is not atomic with respect to other file system * operations. As a result, the path may be left in a partially-deleted * state if this method fails. * * @param path the path to delete * * @throws NoSuchFileException if the path does not exist * @throws IOException if an I/O error occurs while deleting the path */ public static void deleteRecursive(Path path) throws IOException { FileSystemProvider provider = path.getFileSystem().provider(); if (provider instanceof RecursiveDelete) { ((RecursiveDelete) provider).deleteRecursive(path); } else if (!fileTreeDelete(path)) { throw new NoSuchFileException(path.toString()); } } /** * Deletes a path recursively if it exists. * <p> * If the path is a file or an empty directory, this method is equivalent to * {@link Files#deleteIfExists(Path)}. If the path is a non-empty directory, * the directory, all sub-directories, and all child files are deleted. * <p> * This operation is not atomic with respect to other file system * operations. As a result, the path may be left in a partially-deleted * state if this method fails. * * @param path the path to delete * * @return {@code true} if the path was deleted by this method, * {@code false} if the path did not exist * * @throws IOException if an I/O error occurs while deleting the path */ public static boolean deleteRecursiveIfExists(Path path) throws IOException { FileSystemProvider provider = path.getFileSystem().provider(); if (provider instanceof RecursiveDelete) { return ((RecursiveDelete) provider).deleteRecursiveIfExists(path); } else { return fileTreeDelete(path); } } private static boolean fileTreeDelete(Path path) throws IOException { try { Files.walkFileTree(path, new DeleteVisitor()); } catch (NoSuchFileException e) { return false; } return true; } private static final class DeleteVisitor extends SimpleFileVisitor<Path> { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { // visitFile may fail because delete fails or attributes are not // available. Try deleting again for the second case. Throwables.propagateIfInstanceOf(exc, NoSuchFileException.class); try { Files.delete(file); } catch (IOException ignored) { // don't hide the original exception with a failed retry throw exc; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { if (exc != null) { throw exc; } Files.delete(dir); return FileVisitResult.CONTINUE; } } /** * Copy a file to a target file in an efficient way. * <p> * This method is equivalent to * {@link Files#copy(Path, Path, java.nio.file.CopyOption...) * Files.copy(source, target, StandardCopyOptions.REPLACE_EXISTING)}, but * may be significantly faster depending on the file systems of the source * and the target. * * @param source the file to copy * @param target the target file * * @throws IllegalArgumentException if {@code source} is not a regular file * or {@code target} is a directory. * @throws IOException if an IO error occurs while copying */ public static void copyLarge(Path source, Path target) throws IOException { checkArgument(Files.isRegularFile(source), "source (%s) must be a regular file.", source.toAbsolutePath()); checkArgument(!Files.isDirectory(target), "target (%s) cannot be a directory.", target.toAbsolutePath()); FileSystemProvider sourceProvider = source.getFileSystem().provider(); FileSystemProvider targetProvider = target.getFileSystem().provider(); if (sourceProvider instanceof LargeFileCopy) { try { ((LargeFileCopy) sourceProvider).copyLarge(source, target); return; } catch (UnsupportedOperationException e) { // Swallow... fall back to other copy strategies } } if (targetProvider instanceof LargeFileCopy) { try { ((LargeFileCopy) targetProvider).copyLarge(source, target); return; } catch (UnsupportedOperationException e) { // Swallow... fall back to other copy strategies } } // Default Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); } /** * Copy a path to a target path recursively. * <p> * If the source is a file or empty directory, this method is equivalent to * {@link Files#copy(Path, Path, java.nio.file.CopyOption...) * Files.copy(source, target)}. If the source is a non-empty directory, the * directory and all descendants are copied to the target path. The * destination must not exist. * <p> * If an {@code IOException} occurs while copying files and directories, * this method may leave a partial copy at the target path. Users may catch * these exceptions and recursively delete the target path if this is a * problem. * * @param source the path to copy * @param target the target path * * @throws FileAlreadyExistsException if the target already exists * @throws NoSuchFileException if any parents of the target do not exist * @throws IOException if an I/O error occurs while copying */ public static void copyRecursive(Path source, Path target) throws IOException { if (Files.exists(target)) { throw new FileAlreadyExistsException(target.toString()); } if (!Files.exists(target.getParent())) { throw new NoSuchFileException(target.toString()); } Path normalizedSource = source.normalize().toAbsolutePath(); Path normalizedTarget = target.normalize().toAbsolutePath(); if (normalizedTarget.startsWith(normalizedSource)) { throw new FileSystemException( normalizedSource.toString(), normalizedTarget.toString(), "cannot copy a path into itself"); } FileSystemProvider sourceProvider = source.getFileSystem().provider(); FileSystemProvider targetProvider = target.getFileSystem().provider(); if (sourceProvider instanceof RecursiveCopy) { try { ((RecursiveCopy) sourceProvider).copyRecursive(source, target); return; } catch (UnsupportedOperationException e) { // Swallow... fall back to other copy strategies } } if (targetProvider instanceof RecursiveCopy) { try { ((RecursiveCopy) targetProvider).copyRecursive(source, target); return; } catch (UnsupportedOperationException e) { // Swallow... fall back to other copy strategies } } Files.walkFileTree(source, new CopyVisitor(source, target)); } /** * Reads the contents of a file as a string. * * @param path the file to read * @param cs the charset to use for decoding * * @return a string containing the full contents of the file * * @throws IOException if an I/O error occurs while reading a file * * @see Files#readAllBytes(Path) */ public static String readAllString(Path path, Charset cs) throws IOException { return cs.decode(ByteBuffer.wrap(Files.readAllBytes(path))).toString(); } /** * Writes a string to a file. * * @param path the path to the file * @param string the string to write * @param cs the charset to use for encoding * @param options options specifying how the file is opened * * @return the path * * @throws IOException if an I/O error occurs while writing the file * * @see Files#write(Path, Iterable, Charset, OpenOption...) */ public static Path write(Path path, String string, Charset cs, OpenOption... options) throws IOException { try (Writer w = Files.newBufferedWriter(path, cs, options)) { w.write(string); } return path; } /** * Returns the default directory for the given file system. */ public static Path defaultDirectory(FileSystem fs) { return fs.getPath("").toAbsolutePath(); } /** * Adds a permission to a file or directory. * * @param path the path to the file or directory * @param permission the permission to add * * @throws IOException if an I/O error occurs while adding the permission */ public static void addPermission(Path path, PosixFilePermission permission) throws IOException { Set<PosixFilePermission> perms = Files.getPosixFilePermissions(path); if (perms.add(permission)) { Files.setPosixFilePermissions(path, perms); } } /** * Recursively adds a permission to a directory. * * @param path the path to the directory * @param permission the permission to add * * @throws IOException if an I/O error occurs while adding the permission */ public static void addPermissionRecursive(Path path, PosixFilePermission permission) throws IOException { changePermissionsRecursive(path, PermissionChange.ADD, Collections.singleton(permission)); } /** * Adds the {@code OWNER_EXECUTE} permission to a file or directory. * * @param path the path to the file or directory * * @throws IOException if an I/O error occurs while setting the permission */ public static void setExecutable(Path path) throws IOException { addPermission(path, PosixFilePermission.OWNER_EXECUTE); } /** * Removes a permission from a file or directory. * * @param path the path to the file or directory * @param permission the permission to remove * * @throws IOException if an I/O error occurs while removing the permission */ public static void removePermission(Path path, PosixFilePermission permission) throws IOException { Set<PosixFilePermission> perms = Files.getPosixFilePermissions(path); if (perms.remove(permission)) { Files.setPosixFilePermissions(path, perms); } } /** * Recursively removes a permission from a directory. * * @param path the path to the directory * @param permission the permission to remove * * @throws IOException if an I/O error occurs while removing the permission */ public static void removePermissionRecursive(Path path, PosixFilePermission permission) throws IOException { changePermissionsRecursive( path, PermissionChange.REMOVE, Collections.singleton(permission)); } /** * Recursively sets permissions for a directory. * * @param path the path to the directory * @param permissions the desired POSIX file permissions * * @throws IOException if an I/O error occurs while setting the permission */ public static void setPermissionRecursive(Path path, Set<PosixFilePermission> permissions) throws IOException { changePermissionsRecursive(path, PermissionChange.SET, permissions); } private static void changePermissionsRecursive(Path path, PermissionChange change, Set<PosixFilePermission> permissions) throws IOException { FileSystemProvider provider = path.getFileSystem().provider(); if (provider instanceof RecursivePermissions) { RecursivePermissions recursiveProvider = (RecursivePermissions) provider; recursiveProvider.changePermissionsRecursive(path, change, permissions); } else { Files.walkFileTree(path, new PermissionVisitor(change, permissions)); } } private static final class PermissionVisitor extends SimpleFileVisitor<Path> { private final PermissionChange change; private final Set<PosixFilePermission> permissions; PermissionVisitor(PermissionChange change, Set<PosixFilePermission> permissions) { this.change = change; this.permissions = permissions; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { changePermissions(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { if (exc != null) { throw exc; } changePermissions(dir); return FileVisitResult.CONTINUE; } private void changePermissions(Path path) throws IOException { final Set<PosixFilePermission> perms; switch (change) { case ADD: perms = Files.getPosixFilePermissions(path); perms.addAll(permissions); break; case REMOVE: perms = Files.getPosixFilePermissions(path); perms.removeAll(permissions); break; case SET: perms = permissions; break; default: throw new IllegalArgumentException("unknown change: " + change); } Files.setPosixFilePermissions(path, perms); } } /** * Test if the given path is empty. Directories are empty if they contain no * entries. Files are empty if they exist and their * {@linkplain Files#size(Path) size} is zero. Paths that do not exist are * always empty. * * @param path the path * * @return {@code true} if the path is empty * * @throws IOException if an I/O error occurs while accessing the path */ public static boolean isEmpty(Path path) throws IOException { if (Files.isDirectory(path)) { try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) { return !ds.iterator().hasNext(); } } else if (Files.exists(path)) { return Files.size(path) == 0; } else { return true; } } /** * Gets a list of the files and directories in the given directory. The * returned paths are {@linkplain Path#resolve(Path) resolved} against the * given path. * * @param dir the {@code Path} to list * * @throws IOException if an I/O error occurs while listing entries */ public static List<Path> listDirectory(Path dir) throws IOException { try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir)) { return Lists.newArrayList(entries); } } /** * Gets a list of the files in the given directory. The returned paths are * {@linkplain Path#resolve(Path) resolved} against the given path. * * @param dir the {@code Path} to list * * @throws IOException if an I/O error occurs while listing entries */ public static List<Path> listDirectoryFiles(Path dir) throws IOException { try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir, regularFileFilter())) { return Lists.newArrayList(entries); } } private enum ListDirectoryMode { FILES, DIRS; private static Set<ListDirectoryMode> all() { return EnumSet.allOf(ListDirectoryMode.class); } private static Set<ListDirectoryMode> filesOnly() { return EnumSet.of(FILES); } } /** * A {@code FileVisitor} that builds a list of {@code Path} objects it visits. * * @author jyu * @author bkeyes */ private static final class ListVisitor extends SimpleFileVisitor<Path> { private final Set<ListDirectoryMode> mode; private final ArrayList<Path> paths; ListVisitor(Set<ListDirectoryMode> mode) { this.mode = mode; paths = new ArrayList<>(); } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (mode.contains(ListDirectoryMode.DIRS)) { paths.add(dir); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (mode.contains(ListDirectoryMode.FILES)) { paths.add(file); } return FileVisitResult.CONTINUE; } public List<Path> getPaths() { return paths; } } /** * Gets a list of the files and directories in the given directory and all * sub-directories. The returned paths are {@linkplain Path#resolve(Path) * resolved} against the given path. * * @param dir the {@code Path} to list * * @throws IOException if an I/O error occurs while descending the file tree */ public static List<Path> listDirectoryRecursive(Path dir) throws IOException { ListVisitor visitor = new ListVisitor(ListDirectoryMode.all()); Files.walkFileTree(dir, visitor); return visitor.getPaths(); } /** * Gets a list of the files in the given directory and all sub-directories. * The returned paths are {@linkplain Path#resolve(Path) resolved} against * the given path. * * @param dir the {@code Path} to list * * @throws IOException if an I/O error occurs while descending the file tree */ public static List<Path> listDirectoryFilesRecursive(Path dir) throws IOException { ListVisitor visitor = new ListVisitor(ListDirectoryMode.filesOnly()); Files.walkFileTree(dir, visitor); return visitor.getPaths(); } /** * Determines if the given path is associated with the default (local) file * system. */ public static boolean isLocal(Path path) { checkNotNull(path, "path must be non-null"); return path.getFileSystem().equals(FileSystems.getDefault()); } private MoreFiles() { throw new UnsupportedOperationException(); } }