Java tutorial
/* * 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.filesystem.impl; import com.facebook.buck.io.file.MorePaths; import com.facebook.buck.io.file.MorePosixFilePermissions; import com.facebook.buck.io.file.MostFiles; import com.facebook.buck.io.file.PathListing; import com.facebook.buck.io.filesystem.BuckPaths; import com.facebook.buck.io.filesystem.CopySourceMode; import com.facebook.buck.io.filesystem.PathMatcher; import com.facebook.buck.io.filesystem.ProjectFilesystem; import com.facebook.buck.io.filesystem.ProjectFilesystemDelegate; import com.facebook.buck.io.filesystem.RecursiveFileMatcher; import com.facebook.buck.io.windowsfs.WindowsFS; import com.facebook.buck.util.MoreSuppliers; import com.facebook.buck.util.environment.Platform; import com.facebook.buck.util.sha1.Sha1HashCode; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.UnmodifiableIterator; import com.google.common.hash.Hashing; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.channels.Channels; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystem; import java.nio.file.FileSystemLoopException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; 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.FileAttribute; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayDeque; import java.util.Comparator; import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.jar.JarFile; import java.util.jar.Manifest; import javax.annotation.Nullable; /** An injectable service for interacting with the filesystem relative to the project root. */ public class DefaultProjectFilesystem implements ProjectFilesystem { private static final Path EDEN_MAGIC_PATH_ELEMENT = Paths.get(".eden"); private final Path projectRoot; private final BuckPaths buckPaths; private final ImmutableSet<PathMatcher> blackListedPaths; private final ImmutableSet<PathMatcher> blackListedDirectories; /** Supplier that returns an absolute path that is guaranteed to exist. */ private final Supplier<Path> tmpDir; private final ProjectFilesystemDelegate delegate; @Nullable private final WindowsFS winFSInstance; // Defaults to false, and so paths should be valid. @VisibleForTesting protected boolean ignoreValidityOfPaths; /** * For testing purposes, subclasses might want to skip some of the verification done by the * constructor on its arguments. */ protected boolean shouldVerifyConstructorArguments() { return true; } @VisibleForTesting protected DefaultProjectFilesystem(Path root, ProjectFilesystemDelegate projectFilesystemDelegate, @Nullable WindowsFS winFSInstance) { this(root.getFileSystem(), root, ImmutableSet.of(), BuckPaths.createDefaultBuckPaths(root), projectFilesystemDelegate, winFSInstance); } public DefaultProjectFilesystem(FileSystem vfs, Path root, ImmutableSet<PathMatcher> blackListedPaths, BuckPaths buckPaths, ProjectFilesystemDelegate delegate, @Nullable WindowsFS winFSInstance) { if (shouldVerifyConstructorArguments()) { Preconditions.checkArgument(Files.isDirectory(root), "%s must be a directory", root); Preconditions.checkState(vfs.equals(root.getFileSystem())); Preconditions.checkArgument(root.isAbsolute(), "Expected absolute path. Got <%s>.", root); } this.projectRoot = MorePaths.normalize(root); this.delegate = delegate; this.ignoreValidityOfPaths = false; this.blackListedPaths = FluentIterable.from(blackListedPaths).append(FluentIterable.from( // "Path" is Iterable, so avoid adding each segment. // We use the default value here because that's what we've always done. MorePaths.filterForSubpaths( ImmutableSet .of(getCacheDir(root, Optional.of(buckPaths.getCacheDir().toString()), buckPaths)), root)) .append(ImmutableSet.of(buckPaths.getTrashDir())).transform(RecursiveFileMatcher::of)).toSet(); this.buckPaths = buckPaths; this.blackListedDirectories = FluentIterable.from(this.blackListedPaths).filter(RecursiveFileMatcher.class) .transform(matcher -> { Path path = matcher.getPath(); ImmutableSet<Path> filtered = MorePaths.filterForSubpaths(ImmutableSet.of(path), root); if (filtered.isEmpty()) { return path; } return Iterables.getOnlyElement(filtered); }) // TODO(#10068334) So we claim to ignore this path to preserve existing behaviour, but // we really don't end up ignoring it in reality (see extractIgnorePaths). .append(ImmutableSet.of(buckPaths.getBuckOut())).transform(RecursiveFileMatcher::of) .transform(matcher -> (PathMatcher) matcher).append( // RecursiveFileMatcher instances are handled separately above because they all // must be relative to the project root, but all other matchers are not relative // to the root and do not require any special treatment. Iterables.filter(this.blackListedPaths, matcher -> !(matcher instanceof RecursiveFileMatcher))) .toSet(); this.tmpDir = MoreSuppliers.memoize(() -> { Path relativeTmpDir = DefaultProjectFilesystem.this.buckPaths.getTmpDir(); try { mkdirs(relativeTmpDir); } catch (IOException e) { throw new RuntimeException(e); } return relativeTmpDir; }); this.winFSInstance = winFSInstance; if (Platform.detect() == Platform.WINDOWS) { Objects.requireNonNull(this.winFSInstance); } } public static Path getCacheDir(Path root, Optional<String> value, BuckPaths buckPaths) { String cacheDir = value.orElse(root.resolve(buckPaths.getCacheDir()).toString()); Path toReturn = root.getFileSystem().getPath(cacheDir); toReturn = MorePaths.expandHomeDir(toReturn); if (toReturn.isAbsolute()) { return toReturn; } ImmutableSet<Path> filtered = MorePaths.filterForSubpaths(ImmutableSet.of(toReturn), root); if (filtered.isEmpty()) { // OK. For some reason the relative path managed to be out of our directory. return toReturn; } return Iterables.getOnlyElement(filtered); } @Override public DefaultProjectFilesystemView asView() { return new DefaultProjectFilesystemView(this, Paths.get(""), projectRoot, ImmutableMap.of()); } @Override public final Path getRootPath() { return projectRoot; } @Override public ImmutableMap<String, ? extends Object> getDelegateDetails() { return delegate.getDetailsForLogging(); } /** * @return the specified {@code path} resolved against {@link #getRootPath()} to an absolute path. */ @Override public Path resolve(Path path) { return MorePaths.normalize(getPathForRelativePath(path).toAbsolutePath()); } @Override public Path resolve(String path) { return MorePaths.normalize(getRootPath().resolve(path).toAbsolutePath()); } /** Construct a relative path between the project root and a given path. */ @Override public Path relativize(Path path) { return projectRoot.relativize(path); } @Override public ImmutableSet<PathMatcher> getBlacklistedPaths() { return blackListedPaths; } /** @return A {@link ImmutableSet} of {@link PathMatcher} objects to have buck ignore. */ @Override public ImmutableSet<PathMatcher> getIgnorePaths() { return blackListedDirectories; } @Override public Path getPathForRelativePath(Path pathRelativeToProjectRoot) { return delegate.getPathForRelativePath(pathRelativeToProjectRoot); } @Override public Path getPathForRelativePath(String pathRelativeToProjectRoot) { return projectRoot.resolve(pathRelativeToProjectRoot); } /** * @param path Absolute path or path relative to the project root. * @return If {@code path} is relative, it is returned. If it is absolute and is inside the * project root, it is relativized to the project root and returned. Otherwise an absent value * is returned. */ @Override public Optional<Path> getPathRelativeToProjectRoot(Path path) { path = MorePaths.normalize(path); if (path.isAbsolute()) { Path configuredBuckOut = MorePaths.normalize(projectRoot.resolve(buckPaths.getConfiguredBuckOut())); // If the path is in the configured buck-out, it's also part of the filesystem. if (path.startsWith(configuredBuckOut) || path.startsWith(projectRoot)) { return Optional.of(MorePaths.relativize(projectRoot, path)); } else { return Optional.empty(); } } else { return Optional.of(path); } } /** * As {@link #getPathForRelativePath(java.nio.file.Path)}, but with the added twist that the * existence of the path is checked before returning. */ @Override public Path getPathForRelativeExistingPath(Path pathRelativeToProjectRoot) { Path file = getPathForRelativePath(pathRelativeToProjectRoot); if (ignoreValidityOfPaths) { return file; } if (exists(file)) { return file; } throw new RuntimeException(String.format("Not an ordinary file: '%s'.", pathRelativeToProjectRoot)); } @Override public boolean exists(Path pathRelativeToProjectRoot, LinkOption... options) { return delegate.exists(pathRelativeToProjectRoot, options); } @Override public long getFileSize(Path pathRelativeToProjectRoot) throws IOException { Path path = getPathForRelativePath(pathRelativeToProjectRoot); if (!Files.isRegularFile(path)) { throw new IOException("Cannot get size of " + path + " because it is not an ordinary file."); } return Files.size(path); } /** * Deletes a file specified by its path relative to the project root. * * <p>Ignores the failure if the file does not exist. * * @param pathRelativeToProjectRoot path to the file * @return {@code true} if the file was deleted, {@code false} if it did not exist */ @Override public boolean deleteFileAtPathIfExists(Path pathRelativeToProjectRoot) throws IOException { return Files.deleteIfExists(getPathForRelativePath(pathRelativeToProjectRoot)); } /** * Deletes a file specified by its path relative to the project root. * * @param pathRelativeToProjectRoot path to the file */ @Override public void deleteFileAtPath(Path pathRelativeToProjectRoot) throws IOException { Files.delete(getPathForRelativePath(pathRelativeToProjectRoot)); } @Override public Properties readPropertiesFile(Path propertiesFile) throws IOException { Properties properties = new Properties(); if (exists(propertiesFile)) { try (BufferedReader reader = new BufferedReader( new InputStreamReader(newFileInputStream(propertiesFile), Charsets.UTF_8))) { properties.load(reader); } return properties; } else { throw new FileNotFoundException(propertiesFile.toString()); } } /** Checks whether there is a normal file at the specified path. */ @Override public boolean isFile(Path pathRelativeToProjectRoot, LinkOption... options) { return Files.isRegularFile(getPathForRelativePath(pathRelativeToProjectRoot), options); } @Override public boolean isHidden(Path pathRelativeToProjectRoot) throws IOException { return Files.isHidden(getPathForRelativePath(pathRelativeToProjectRoot)); } /** * Similar to {@link #walkFileTree(Path, FileVisitor)} except this takes in a path relative to the * project root. */ @Override public void walkRelativeFileTree(Path pathRelativeToProjectRoot, FileVisitor<Path> fileVisitor) throws IOException { walkRelativeFileTree(pathRelativeToProjectRoot, fileVisitor, true); } @Override public void walkRelativeFileTree(Path pathRelativeToProjectRoot, FileVisitor<Path> fileVisitor, boolean skipIgnored) throws IOException { walkRelativeFileTree(pathRelativeToProjectRoot, EnumSet.of(FileVisitOption.FOLLOW_LINKS), fileVisitor, skipIgnored); } @Override public void walkRelativeFileTree(Path pathRelativeToProjectRoot, EnumSet<FileVisitOption> visitOptions, FileVisitor<Path> fileVisitor) throws IOException { walkRelativeFileTree(pathRelativeToProjectRoot, visitOptions, fileVisitor, true); } /** Walks a project-root relative file tree with a visitor and visit options. */ @Override public void walkRelativeFileTree(Path pathRelativeToProjectRoot, EnumSet<FileVisitOption> visitOptions, FileVisitor<Path> fileVisitor, boolean skipIgnored) throws IOException { walkRelativeFileTree(pathRelativeToProjectRoot, visitOptions, fileVisitor, skipIgnored ? input -> !isIgnored(relativize(input)) : input -> true); } void walkRelativeFileTree(Path pathRelativeToProjectRoot, EnumSet<FileVisitOption> visitOptions, FileVisitor<Path> fileVisitor, DirectoryStream.Filter<? super Path> ignoreFilter) throws IOException { Path rootPath = getPathForRelativePath(pathRelativeToProjectRoot); walkFileTreeWithPathMapping(rootPath, visitOptions, fileVisitor, ignoreFilter, path -> relativize(path)); } void walkFileTreeWithPathMapping(Path root, EnumSet<FileVisitOption> visitOptions, FileVisitor<Path> fileVisitor, DirectoryStream.Filter<? super Path> ignoreFilter, Function<Path, Path> pathMapper) throws IOException { FileVisitor<Path> pathMappingVisitor = new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { // TODO(mbolin): We should not have hardcoded logic for Eden here. Instead, we should // properly handle cyclic symlinks in a general way. // Failure to perform this check will result in a java.nio.file.FileSystemLoopException // in Eden. if (EDEN_MAGIC_PATH_ELEMENT.equals(dir.getFileName())) { return FileVisitResult.SKIP_SUBTREE; } return fileVisitor.preVisitDirectory(pathMapper.apply(dir), attrs); } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { return fileVisitor.visitFile(pathMapper.apply(file), attrs); } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return fileVisitor.visitFileFailed(pathMapper.apply(file), exc); } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return fileVisitor.postVisitDirectory(pathMapper.apply(dir), exc); } }; walkFileTree(root, visitOptions, pathMappingVisitor, ignoreFilter); } /** Allows {@link Files#walkFileTree} to be faked in tests. */ @Override public void walkFileTree(Path root, FileVisitor<Path> fileVisitor) throws IOException { walkFileTree(root, EnumSet.noneOf(FileVisitOption.class), fileVisitor); } @Override public void walkFileTree(Path root, Set<FileVisitOption> options, FileVisitor<Path> fileVisitor) throws IOException { walkFileTree(root, options, fileVisitor, true); } @Override public void walkFileTree(Path root, Set<FileVisitOption> options, FileVisitor<Path> fileVisitor, boolean skipIgnored) throws IOException { walkFileTree(root, options, fileVisitor, skipIgnored ? input -> !isIgnored(relativize(input)) : input -> true); } void walkFileTree(Path root, Set<FileVisitOption> options, FileVisitor<Path> fileVisitor, DirectoryStream.Filter<? super Path> ignoreFilter) throws IOException { root = getPathForRelativePath(root); new FileTreeWalker(root, options, fileVisitor, ignoreFilter).walk(); } @Override public ImmutableSet<Path> getFilesUnderPath(Path pathRelativeToProjectRoot) throws IOException { return getFilesUnderPath(pathRelativeToProjectRoot, x -> true); } @Override public ImmutableSet<Path> getFilesUnderPath(Path pathRelativeToProjectRoot, Predicate<Path> predicate) throws IOException { return getFilesUnderPath(pathRelativeToProjectRoot, predicate, EnumSet.of(FileVisitOption.FOLLOW_LINKS)); } @Override public ImmutableSet<Path> getFilesUnderPath(Path pathRelativeToProjectRoot, Predicate<Path> predicate, EnumSet<FileVisitOption> visitOptions) throws IOException { ImmutableSet.Builder<Path> paths = ImmutableSet.builder(); walkRelativeFileTree(pathRelativeToProjectRoot, visitOptions, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attributes) { if (predicate.test(path)) { paths.add(path); } return FileVisitResult.CONTINUE; } }); return paths.build(); } /** Allows {@link Files#isDirectory} to be faked in tests. */ @Override public boolean isDirectory(Path child, LinkOption... linkOptions) { return MorePaths.isDirectory(getPathForRelativePath(child), linkOptions); } /** Allows {@link Files#isExecutable} to be faked in tests. */ @Override public boolean isExecutable(Path child) { return delegate.isExecutable(child); } @Override public ImmutableCollection<Path> getDirectoryContents(Path pathToUse) throws IOException { Path path = getPathForRelativePath(pathToUse); try (DirectoryStream<Path> stream = getDirectoryContentsStream(path)) { return FluentIterable.from(stream).filter(input -> !isIgnored(relativize(input))) .transform(absolutePath -> MorePaths.relativize(projectRoot, absolutePath)) .toSortedList(Comparator.naturalOrder()); } } /** @return returns sorted absolute paths of everything under the given directory */ DirectoryStream<Path> getDirectoryContentsStream(Path absolutePath) throws IOException { return Files.newDirectoryStream(absolutePath); } @VisibleForTesting protected PathListing.PathModifiedTimeFetcher getLastModifiedTimeFetcher() { return path -> DefaultProjectFilesystem.this.getLastModifiedTime(path); } /** * Returns the files inside {@code pathRelativeToProjectRoot} which match {@code globPattern}, * ordered in descending last modified time order. This will not obey the results of {@link * #isIgnored(Path)}. */ @Override public ImmutableSortedSet<Path> getMtimeSortedMatchingDirectoryContents(Path pathRelativeToProjectRoot, String globPattern) throws IOException { Path path = getPathForRelativePath(pathRelativeToProjectRoot); return PathListing.listMatchingPaths(path, globPattern, getLastModifiedTimeFetcher()); } @Override public FileTime getLastModifiedTime(Path pathRelativeToProjectRoot) throws IOException { Path path = getPathForRelativePath(pathRelativeToProjectRoot); return Files.getLastModifiedTime(path); } /** Sets the last modified time for the given path. */ @Override public Path setLastModifiedTime(Path pathRelativeToProjectRoot, FileTime time) throws IOException { Path path = getPathForRelativePath(pathRelativeToProjectRoot); return Files.setLastModifiedTime(path, time); } /** * Recursively delete everything under the specified path. Ignore the failure if the file at the * specified path does not exist. */ @Override public void deleteRecursivelyIfExists(Path pathRelativeToProjectRoot) throws IOException { MostFiles.deleteRecursivelyIfExists(resolve(pathRelativeToProjectRoot)); } /** * Resolves the relative path against the project root and then calls {@link * Files#createDirectories(java.nio.file.Path, java.nio.file.attribute.FileAttribute[])} */ @Override public void mkdirs(Path pathRelativeToProjectRoot) throws IOException { Path resolved = resolve(pathRelativeToProjectRoot); try { Files.createDirectories(resolved); } catch (FileAlreadyExistsException e) { // Don't complain if the file is a symlink that points to a valid directory. // This check is done only on exception as it's a rare case, and lstat is not free. if (!Files.isDirectory(resolved)) { throw e; } } } /** Creates a new file relative to the project root. */ @Override public Path createNewFile(Path pathRelativeToProjectRoot) throws IOException { Path path = getPathForRelativePath(pathRelativeToProjectRoot); return Files.createFile(path); } /** * // @deprecated Prefer operating on {@code Path}s directly, replaced by {@link * #createParentDirs(java.nio.file.Path)}. */ @Override public void createParentDirs(String pathRelativeToProjectRoot) throws IOException { Path file = getPathForRelativePath(pathRelativeToProjectRoot); mkdirs(file.getParent()); } /** * @param pathRelativeToProjectRoot Must identify a file, not a directory. (Unfortunately, we have * no way to assert this because the path is not expected to exist yet.) */ @Override public void createParentDirs(Path pathRelativeToProjectRoot) throws IOException { Path file = resolve(pathRelativeToProjectRoot); Path directory = file.getParent(); mkdirs(directory); } /** * Writes each line in {@code lines} with a trailing newline to a file at the specified path. * * <p>The parent path of {@code pathRelativeToProjectRoot} must exist. */ @Override public void writeLinesToPath(Iterable<String> lines, Path pathRelativeToProjectRoot, FileAttribute<?>... attrs) throws IOException { try (Writer writer = new BufferedWriter( new OutputStreamWriter(newFileOutputStream(pathRelativeToProjectRoot, attrs), Charsets.UTF_8))) { for (String line : lines) { writer.write(line); writer.write('\n'); } } } @Override public void writeContentsToPath(String contents, Path pathRelativeToProjectRoot, FileAttribute<?>... attrs) throws IOException { writeBytesToPath(contents.getBytes(Charsets.UTF_8), pathRelativeToProjectRoot, attrs); } @Override public void writeBytesToPath(byte[] bytes, Path pathRelativeToProjectRoot, FileAttribute<?>... attrs) throws IOException { // No need to buffer writes when writing a single piece of data. try (OutputStream outputStream = newUnbufferedFileOutputStream(pathRelativeToProjectRoot, /* append */ false, attrs)) { outputStream.write(bytes); } } @Override public OutputStream newFileOutputStream(Path pathRelativeToProjectRoot, FileAttribute<?>... attrs) throws IOException { return newFileOutputStream(pathRelativeToProjectRoot, /* append */ false, attrs); } @Override public OutputStream newFileOutputStream(Path pathRelativeToProjectRoot, boolean append, FileAttribute<?>... attrs) throws IOException { return new BufferedOutputStream(newUnbufferedFileOutputStream(pathRelativeToProjectRoot, append, attrs)); } @Override public OutputStream newUnbufferedFileOutputStream(Path pathRelativeToProjectRoot, boolean append, FileAttribute<?>... attrs) throws IOException { return Channels .newOutputStream( Files.newByteChannel(getPathForRelativePath(pathRelativeToProjectRoot), append ? ImmutableSet.of(StandardOpenOption.CREATE, StandardOpenOption.APPEND) : ImmutableSet.of(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE), attrs)); } @Override public <A extends BasicFileAttributes> A readAttributes(Path pathRelativeToProjectRoot, Class<A> type, LinkOption... options) throws IOException { return Files.readAttributes(getPathForRelativePath(pathRelativeToProjectRoot), type, options); } @Override public InputStream newFileInputStream(Path pathRelativeToProjectRoot) throws IOException { return new BufferedInputStream(Files.newInputStream(getPathForRelativePath(pathRelativeToProjectRoot))); } /** @param inputStream Source of the bytes. This method does not close this stream. */ @Override public void copyToPath(InputStream inputStream, Path pathRelativeToProjectRoot, CopyOption... options) throws IOException { Files.copy(inputStream, getPathForRelativePath(pathRelativeToProjectRoot), options); } /** Copies a file to an output stream. */ @Override public void copyToOutputStream(Path pathRelativeToProjectRoot, OutputStream out) throws IOException { Files.copy(getPathForRelativePath(pathRelativeToProjectRoot), out); } @Override public Optional<String> readFileIfItExists(Path pathRelativeToProjectRoot) { Path fileToRead = getPathForRelativePath(pathRelativeToProjectRoot); return readFileIfItExists(fileToRead, pathRelativeToProjectRoot.toString()); } private Optional<String> readFileIfItExists(Path fileToRead, String pathRelativeToProjectRoot) { if (Files.isRegularFile(fileToRead)) { String contents; try { contents = new String(Files.readAllBytes(fileToRead), Charsets.UTF_8); } catch (IOException e) { // Alternatively, we could return Optional.empty(), though something seems suspicious if we // have already verified that fileToRead is a file and then we cannot read it. throw new RuntimeException("Error reading " + pathRelativeToProjectRoot, e); } return Optional.of(contents); } else { return Optional.empty(); } } /** * Attempts to read the first line of the file specified by the relative path. If the file does * not exist, is empty, or encounters an error while being read, {@link Optional#empty()} is * returned. Otherwise, an {@link Optional} with the first line of the file will be returned. * * <p>// @deprecated PRefero operation on {@code Path}s directly, replaced by {@link * #readFirstLine(java.nio.file.Path)} */ @Override public Optional<String> readFirstLine(String pathRelativeToProjectRoot) { return readFirstLine(projectRoot.getFileSystem().getPath(pathRelativeToProjectRoot)); } /** * Attempts to read the first line of the file specified by the relative path. If the file does * not exist, is empty, or encounters an error while being read, {@link Optional#empty()} is * returned. Otherwise, an {@link Optional} with the first line of the file will be returned. */ @Override public Optional<String> readFirstLine(Path pathRelativeToProjectRoot) { Path file = getPathForRelativePath(pathRelativeToProjectRoot); return readFirstLineFromFile(file); } /** * Attempts to read the first line of the specified file. If the file does not exist, is empty, or * encounters an error while being read, {@link Optional#empty()} is returned. Otherwise, an * {@link Optional} with the first line of the file will be returned. */ @Override public Optional<String> readFirstLineFromFile(Path file) { try { try (BufferedReader reader = Files.newBufferedReader(file, Charsets.UTF_8)) { return Optional.ofNullable(reader.readLine()); } } catch (IOException e) { // Because the file is not even guaranteed to exist, swallow the IOException. return Optional.empty(); } } @Override public List<String> readLines(Path pathRelativeToProjectRoot) throws IOException { Path file = getPathForRelativePath(pathRelativeToProjectRoot); return Files.readAllLines(file, Charsets.UTF_8); } /** * // @deprecated Prefer operation on {@code Path}s directly, replaced by {@link * Files#newInputStream(java.nio.file.Path, java.nio.file.OpenOption...)}. */ @Override public InputStream getInputStreamForRelativePath(Path path) throws IOException { Path file = getPathForRelativePath(path); return Files.newInputStream(file); } @Override public Sha1HashCode computeSha1(Path pathRelativeToProjectRootOrJustAbsolute) throws IOException { return delegate.computeSha1(pathRelativeToProjectRootOrJustAbsolute); } @Override public String computeSha256(Path pathRelativeToProjectRoot) throws IOException { Path fileToHash = getPathForRelativePath(pathRelativeToProjectRoot); return Hashing.sha256().hashBytes(Files.readAllBytes(fileToHash)).toString(); } @Override public void copy(Path source, Path target, CopySourceMode sourceMode) throws IOException { source = getPathForRelativePath(source); switch (sourceMode) { case FILE: Files.copy(resolve(source), resolve(target), StandardCopyOption.REPLACE_EXISTING); break; case DIRECTORY_CONTENTS_ONLY: MostFiles.copyRecursively(resolve(source), resolve(target)); break; case DIRECTORY_AND_CONTENTS: MostFiles.copyRecursively(resolve(source), resolve(target.resolve(source.getFileName()))); break; } } @Override public void move(Path source, Path target, CopyOption... options) throws IOException { Files.move(resolve(source), resolve(target), options); } @Override public void mergeChildren(Path source, Path target, CopyOption... options) throws IOException { Path resolvedSource = resolve(source); Path resolvedTarget = resolve(target); walkFileTree(resolvedSource, new FileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path relative = resolvedSource.relativize(dir); Path destDir = resolvedTarget.resolve(relative); if (!Files.exists(destDir)) { // Short circuit any copying Files.move(dir, destDir, options); return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (!attrs.isDirectory()) { Path relative = resolvedSource.relativize(file); Files.move(file, resolvedTarget.resolve(relative), options); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) { return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { if (!dir.equals(resolvedSource)) { Files.deleteIfExists(dir); } return FileVisitResult.CONTINUE; } }); } @Override public void copyFolder(Path source, Path target) throws IOException { copy(source, target, CopySourceMode.DIRECTORY_CONTENTS_ONLY); } @Override public void copyFile(Path source, Path target) throws IOException { copy(source, target, CopySourceMode.FILE); } @Override public void createSymLink(Path symLink, Path realFile, boolean force) throws IOException { symLink = resolve(symLink); if (force) { MostFiles.deleteRecursivelyIfExists(symLink); } MorePaths.createSymLink(winFSInstance, symLink, realFile); } /** * Returns the set of POSIX file permissions, or the empty set if the underlying file system does * not support POSIX file attributes. */ @Override public Set<PosixFilePermission> getPosixFilePermissions(Path path) throws IOException { Path resolvedPath = getPathForRelativePath(path); if (Files.getFileAttributeView(resolvedPath, PosixFileAttributeView.class) != null) { return Files.getPosixFilePermissions(resolvedPath); } else { return ImmutableSet.of(); } } /** Returns true if the file under {@code path} exists and is a symbolic link, false otherwise. */ @Override public boolean isSymLink(Path path) { return delegate.isSymlink(path); } /** Returns the target of the specified symbolic link. */ @Override public Path readSymLink(Path path) throws IOException { return Files.readSymbolicLink(getPathForRelativePath(path)); } @Override public Manifest getJarManifest(Path path) throws IOException { Path absolutePath = getPathForRelativePath(path); try (JarFile jarFile = new JarFile(absolutePath.toFile())) { return jarFile.getManifest(); } } @Override public long getPosixFileMode(Path path) throws IOException { long mode = 0; // Support executable files. If we detect this file is executable, store this // information as 0100 in the field typically used in zip implementations for // POSIX file permissions. We'll use this information when unzipping. if (isExecutable(path)) { mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.OWNER_EXECUTE)); mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.GROUP_EXECUTE)); mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.OTHERS_EXECUTE)); } if (Files.isReadable(path)) { mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.OWNER_READ)); mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.GROUP_READ)); mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.OTHERS_READ)); } if (Files.isWritable(path)) { mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.OWNER_WRITE)); mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.GROUP_WRITE)); mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.OTHERS_WRITE)); } if (isDirectory(path)) { mode |= MostFiles.S_IFDIR; if (Files.isReadable(path)) { mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.OWNER_EXECUTE)); mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.GROUP_EXECUTE)); mode |= MorePosixFilePermissions.toMode(EnumSet.of(PosixFilePermission.OTHERS_EXECUTE)); } } else if (isFile(path)) { mode |= MostFiles.S_IFREG; } // Propagate any additional permissions mode |= MorePosixFilePermissions.toMode(getPosixFilePermissions(path)); return mode; } @Override public long getFileAttributesForZipEntry(Path path) throws IOException { return getPosixFileMode(path) << 16; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof DefaultProjectFilesystem)) { return false; } DefaultProjectFilesystem that = (DefaultProjectFilesystem) other; if (!Objects.equals(projectRoot, that.projectRoot)) { return false; } return Objects.equals(blackListedPaths, that.blackListedPaths); } @Override public String toString() { return String.format("%s (projectRoot=%s, hash(blackListedPaths)=%s)", super.toString(), projectRoot, blackListedPaths.hashCode()); } @Override public int hashCode() { return Objects.hash(projectRoot, blackListedPaths); } @Override public BuckPaths getBuckPaths() { return buckPaths; } /** * @param path the path to check. * @return whether ignoredPaths contains path or any of its ancestors. */ @Override public boolean isIgnored(Path path) { Preconditions.checkArgument(!path.isAbsolute()); for (PathMatcher blackListedPath : blackListedPaths) { if (blackListedPath.matches(path)) { return true; } } return false; } /** * Returns a relative path whose parent directory is guaranteed to exist. The path will be under * {@code buck-out}, so it is safe to write to. */ @Override public Path createTempFile(String prefix, String suffix, FileAttribute<?>... attrs) throws IOException { return createTempFile(tmpDir.get(), prefix, suffix, attrs); } /** * Prefer {@link #createTempFile(String, String, FileAttribute[])} so that temporary files are * guaranteed to be created under {@code buck-out}. This method will be deprecated once t12079608 * is resolved. */ @Override public Path createTempFile(Path directory, String prefix, String suffix, FileAttribute<?>... attrs) throws IOException { Path tmp = Files.createTempFile(resolve(directory), prefix, suffix, attrs); return getPathRelativeToProjectRoot(tmp).orElse(tmp); } @Override public void touch(Path fileToTouch) throws IOException { if (exists(fileToTouch)) { setLastModifiedTime(fileToTouch, FileTime.fromMillis(System.currentTimeMillis())); } else { createNewFile(fileToTouch); } } /** * Converts a path string (or sequence of strings) to a Path with the same VFS as this instance. * * @see FileSystem#getPath(String, String...) */ @Override public Path getPath(String first, String... rest) { return getRootPath().getFileSystem().getPath(first, rest); } /** * FileTreeWalker is used to walk files similar to Files.walkFileTree. * * <p>It has two major differences from walkFileTree. 1. It ignores files and directories ignored * by this ProjectFilesystem. 2. The walk is in a deterministic order. * * <p>And it has two minor differences. 1. It doesn't accept a depth limit. 2. It doesn't handle * the presence of a security manager the same way. */ private class FileTreeWalker { private final FileVisitor<Path> visitor; private final Path root; private final boolean followLinks; private final ArrayDeque<DirWalkState> state; private final Filter<? super Path> ignoreFilter; FileTreeWalker(Path root, Set<FileVisitOption> options, FileVisitor<Path> pathFileVisitor, DirectoryStream.Filter<? super Path> ignoreFilter) { this.followLinks = options.contains(FileVisitOption.FOLLOW_LINKS); this.visitor = pathFileVisitor; this.root = root; this.state = new ArrayDeque<>(); this.ignoreFilter = ignoreFilter; } private ImmutableList<Path> getContents(Path root) throws IOException { try (DirectoryStream<Path> stream = Files.newDirectoryStream(root, ignoreFilter)) { return FluentIterable.from(stream).toSortedList(Comparator.naturalOrder()); } } private class DirWalkState { final Path dir; final BasicFileAttributes attrs; final boolean isRootSentinel; UnmodifiableIterator<Path> iter; @Nullable IOException ioe = null; DirWalkState(Path directory, BasicFileAttributes attributes, boolean isRootSentinel) { this.dir = directory; this.attrs = attributes; if (isRootSentinel) { this.iter = ImmutableList.of(root).iterator(); } else { try { this.iter = getContents(directory).iterator(); } catch (IOException e) { this.iter = ImmutableList.<Path>of().iterator(); this.ioe = e; } } this.isRootSentinel = isRootSentinel; } } private void walk() throws IOException { state.add(new DirWalkState(root, getAttributes(root), true)); while (true) { FileVisitResult result; if (state.getLast().iter.hasNext()) { result = visitPath(state.getLast().iter.next()); } else { DirWalkState dirState = state.removeLast(); if (dirState.isRootSentinel) { return; } result = visitor.postVisitDirectory(dirState.dir, dirState.ioe); } Objects.requireNonNull(result, "FileVisitor returned a null FileVisitResult."); if (result == FileVisitResult.SKIP_SIBLINGS) { state.getLast().iter = ImmutableList.<Path>of().iterator(); } else if (result == FileVisitResult.TERMINATE) { return; } } } private FileVisitResult visitPath(Path p) throws IOException { BasicFileAttributes attrs; try { attrs = getAttributes(p); ensureNoLoops(p, attrs); } catch (IOException ioe) { return visitor.visitFileFailed(p, ioe); } if (attrs.isDirectory()) { FileVisitResult result = visitor.preVisitDirectory(p, attrs); if (result == FileVisitResult.CONTINUE) { state.add(new DirWalkState(p, attrs, false)); } return result; } else { return visitor.visitFile(p, attrs); } } private void ensureNoLoops(Path p, BasicFileAttributes attrs) throws FileSystemLoopException { if (!followLinks) { return; } if (!attrs.isDirectory()) { return; } if (willLoop(p, attrs)) { throw new FileSystemLoopException(p.toString()); } } private boolean willLoop(Path p, BasicFileAttributes attrs) { try { Object thisKey = attrs.fileKey(); for (DirWalkState s : state) { if (s.isRootSentinel) { continue; } Object thatKey = s.attrs.fileKey(); if (thisKey != null && thatKey != null) { if (thisKey.equals(thatKey)) { return true; } } else if (Files.isSameFile(p, s.dir)) { return true; } } } catch (IOException e) { return true; } return false; } private BasicFileAttributes getAttributes(Path root) throws IOException { if (!followLinks) { return Files.readAttributes(root, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); } try { return Files.readAttributes(root, BasicFileAttributes.class); } catch (IOException e) { return Files.readAttributes(root, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); } } } }