/* * Copyright 2012-present Facebook, 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.facebook.buck.io; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Objects; import com.google.common.io.ByteStreams; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.UUID; public final class MoreFiles { // Extended attribute bits for directories and symlinks; see: // http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute @SuppressWarnings("PMD.AvoidUsingOctalValues") public static final long S_IFDIR = 0040000; @SuppressWarnings("PMD.AvoidUsingOctalValues") public static final long S_IFREG = 0100000; @SuppressWarnings("PMD.AvoidUsingOctalValues") public static final long S_IFLNK = 0120000; public enum DeleteRecursivelyOptions { IGNORE_NO_SUCH_FILE_EXCEPTION, DELETE_CONTENTS_ONLY, } // Unix has two illegal characters - '/', and '\0'. Windows has ten, which includes those two. // The full list can be found at https://msdn.microsoft.com/en-us/library/aa365247 private static final String ILLEGAL_FILE_NAME_CHARACTERS = "<>:\"/\\|?*\0"; private static class FileAccessedEntry { public final File file; public final FileTime lastAccessTime; public File getFile() { return file; } public FileTime getLastAccessTime() { return lastAccessTime; } private FileAccessedEntry(File file, FileTime lastAccessTime) { this.file = file; this.lastAccessTime = lastAccessTime; } } /** Sorts by the lastAccessTime in descending order (more recently accessed files are first). */ private static final Comparator<FileAccessedEntry> SORT_BY_LAST_ACCESSED_TIME_DESC = (a, b) -> b.getLastAccessTime().compareTo(a.getLastAccessTime()); /** Utility class: do not instantiate. */ private MoreFiles() {} public static void deleteRecursivelyIfExists(Path path) throws IOException { deleteRecursivelyWithOptions( path, EnumSet.of(DeleteRecursivelyOptions.IGNORE_NO_SUCH_FILE_EXCEPTION)); } /** Recursively copies all files under {@code fromPath} to {@code toPath}. */ public static void copyRecursively(final Path fromPath, final Path toPath) throws IOException { copyRecursively(fromPath, toPath, Functions.identity()); } /** Recursively copies all files under {@code fromPath} to {@code toPath}. */ public static void copyRecursivelyWithFilter( final Path fromPath, final Path toPath, final Function<Path, Boolean> filter) throws IOException { copyRecursively(fromPath, toPath, Functions.identity(), filter); } /** * Recursively copies all files under {@code fromPath} to {@code toPath}. The {@code transform} * will be applied after the destination path for a file has been relativized. * * @param fromPath item to copy * @param toPath destination of copy * @param transform renaming function to apply when copying. If this function returns null, then * the file is not copied. */ public static void copyRecursively( final Path fromPath, final Path toPath, final Function<Path, Path> transform) throws IOException { copyRecursively(fromPath, toPath, transform, input -> true); } public static void copyRecursively( final Path fromPath, final Path toPath, final Function<Path, Path> transform, final Function<Path, Boolean> filter) throws IOException { // Adapted from http://codingjunkie.net/java-7-copy-move/. SimpleFileVisitor<Path> copyDirVisitor = new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path targetPath = toPath.resolve(fromPath.relativize(dir)); if (!Files.exists(targetPath)) { Files.createDirectory(targetPath); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (!filter.apply(file)) { return FileVisitResult.CONTINUE; } Path destPath = toPath.resolve(fromPath.relativize(file)); Path transformedDestPath = transform.apply(destPath); if (transformedDestPath != null) { if (Files.isSymbolicLink(file)) { Files.deleteIfExists(transformedDestPath); Files.createSymbolicLink(transformedDestPath, Files.readSymbolicLink(file)); } else { Files.copy(file, transformedDestPath, StandardCopyOption.REPLACE_EXISTING); } } return FileVisitResult.CONTINUE; } }; Files.walkFileTree(fromPath, copyDirVisitor); } public static void deleteRecursively(final Path path) throws IOException { deleteRecursivelyWithOptions(path, EnumSet.noneOf(DeleteRecursivelyOptions.class)); } public static void deleteRecursivelyWithOptions( final Path path, final EnumSet<DeleteRecursivelyOptions> options) throws IOException { try { // Adapted from http://codingjunkie.net/java-7-copy-move/. SimpleFileVisitor<Path> deleteDirVisitor = new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { if (e == null) { // Allow leaving the top-level directory in place (e.g. for deleting the contents of // the trash dir but not the trash dir itself) if (!(options.contains(DeleteRecursivelyOptions.DELETE_CONTENTS_ONLY) && dir.equals(path))) { Files.delete(dir); } return FileVisitResult.CONTINUE; } else { throw e; } } }; Files.walkFileTree(path, deleteDirVisitor); } catch (NoSuchFileException e) { if (!options.contains(DeleteRecursivelyOptions.IGNORE_NO_SUCH_FILE_EXCEPTION)) { throw e; } } } /** Writes the specified lines to the specified file, encoded as UTF-8. */ public static void writeLinesToFile(Iterable<String> lines, Path file) throws IOException { try (BufferedWriter writer = Files.newBufferedWriter(file, Charsets.UTF_8)) { for (String line : lines) { writer.write(line); writer.newLine(); } } } /** Log a simplistic diff between lines and the contents of file. */ @VisibleForTesting static List<String> diffFileContents(Iterable<String> lines, File file) throws IOException { List<String> diffLines = new ArrayList<>(); Iterator<String> iter = lines.iterator(); try (BufferedReader reader = Files.newBufferedReader(file.toPath(), Charsets.UTF_8)) { while (iter.hasNext()) { String lineA = reader.readLine(); String lineB = iter.next(); if (!Objects.equal(lineA, lineB)) { diffLines.add(String.format("| %s | %s |", lineA == null ? "" : lineA, lineB)); } } String lineA; while ((lineA = reader.readLine()) != null) { diffLines.add(String.format("| %s | |", lineA)); } } return diffLines; } /** * Does an in-place sort of the specified {@code files} array. Most recently accessed files will * be at the front of the array when sorted. */ public static void sortFilesByAccessTime(File[] files) { FileAccessedEntry[] fileAccessedEntries = new FileAccessedEntry[files.length]; for (int i = 0; i < files.length; ++i) { FileTime lastAccess; try { lastAccess = Files.readAttributes(files[i].toPath(), BasicFileAttributes.class).lastAccessTime(); } catch (IOException e) { lastAccess = FileTime.fromMillis(files[i].lastModified()); } fileAccessedEntries[i] = new FileAccessedEntry(files[i], lastAccess); } Arrays.sort(fileAccessedEntries, SORT_BY_LAST_ACCESSED_TIME_DESC); for (int i = 0; i < files.length; i++) { files[i] = fileAccessedEntries[i].getFile(); } } /** * Tries to make the specified file executable. For file systems that do support the POSIX-style * permissions, the executable permission is set for each category of users that already has the * read permission. * * <p>If the file system does not support the executable permission or the operation fails, a * {@code java.io.IOException} is thrown. */ public static void makeExecutable(Path file) throws IOException { if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(file); if (permissions.contains(PosixFilePermission.OWNER_READ)) { permissions.add(PosixFilePermission.OWNER_EXECUTE); } if (permissions.contains(PosixFilePermission.GROUP_READ)) { permissions.add(PosixFilePermission.GROUP_EXECUTE); } if (permissions.contains(PosixFilePermission.OTHERS_READ)) { permissions.add(PosixFilePermission.OTHERS_EXECUTE); } Files.setPosixFilePermissions(file, permissions); } else { if (!file.toFile().setExecutable(/* executable */ true, /* ownerOnly */ true)) { throw new IOException("The file could not be made executable"); } } } /** * Given a file name, replace any illegal characters from it. * * @param name The file name to sanitize * @return a properly sanitized filename */ public static String sanitize(String name) { return CharMatcher.anyOf(ILLEGAL_FILE_NAME_CHARACTERS).replaceFrom(name, "_"); } /** * Concatenates the contents of one or more files. * * @param dest The path to which the concatenated files' contents are written. * @param pathsToConcatenate The paths whose contents are concatenated to {@code dest}. * @return {@code true} if any data was concatenated to {@code dest}, {@code false} otherwise. */ public static boolean concatenateFiles(Path dest, Iterable<Path> pathsToConcatenate) throws IOException { // Concatenate all the logs to a temp file, then atomically rename it to the // passed-in concatenatedPath if any log data was collected. String tempFilename = "." + dest.getFileName() + ".tmp." + UUID.randomUUID().toString(); Path tempPath = dest.resolveSibling(tempFilename); try { long bytesCollected = 0; try (OutputStream os = Files.newOutputStream( tempPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { for (Path path : pathsToConcatenate) { try (InputStream is = Files.newInputStream(path)) { bytesCollected += ByteStreams.copy(is, os); } catch (NoSuchFileException e) { continue; } } } if (bytesCollected > 0) { Files.move( tempPath, dest, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); return true; } else { return false; } } finally { Files.deleteIfExists(tempPath); } } }