| // Copyright 2014 The Bazel Authors. All rights reserved. |
| // |
| // 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.google.devtools.build.lib.vfs; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.devtools.build.lib.clock.Clock; |
| import com.google.devtools.build.lib.clock.JavaClock; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; |
| import com.google.devtools.build.lib.profiler.Profiler; |
| import com.google.devtools.build.lib.profiler.ProfilerTask; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.nio.file.Files; |
| import java.nio.file.InvalidPathException; |
| import java.nio.file.LinkOption; |
| import java.nio.file.Paths; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A FileSystem that does not use any JNI and hence, does not require a shared library be present at |
| * execution. |
| * |
| * <p>Note: Blaze profiler tasks are defined on the system call level - thus we do not distinguish |
| * (from profiling perspective) between different methods on this class that end up doing stat() |
| * system call - they all are associated with the VFS_STAT task. |
| */ |
| @ThreadSafe |
| public class JavaIoFileSystem extends AbstractFileSystemWithCustomStat { |
| private static final LinkOption[] NO_LINK_OPTION = new LinkOption[0]; |
| // This isn't generally safe; we rely on the file system APIs not modifying the array. |
| private static final LinkOption[] NOFOLLOW_LINKS_OPTION = |
| new LinkOption[] { LinkOption.NOFOLLOW_LINKS }; |
| |
| private final Clock clock; |
| |
| protected static final String ERR_IS_DIRECTORY = " (Is a directory)"; |
| protected static final String ERR_DIRECTORY_NOT_EMPTY = " (Directory not empty)"; |
| protected static final String ERR_FILE_EXISTS = " (File exists)"; |
| protected static final String ERR_NO_SUCH_FILE_OR_DIR = " (No such file or directory)"; |
| protected static final String ERR_NOT_A_DIRECTORY = " (Not a directory)"; |
| |
| public JavaIoFileSystem(DigestHashFunction hashFunction) { |
| super(hashFunction); |
| this.clock = new JavaClock(); |
| } |
| |
| @VisibleForTesting |
| JavaIoFileSystem(Clock clock, DigestHashFunction hashFunction) { |
| super(hashFunction); |
| this.clock = clock; |
| } |
| |
| protected File getIoFile(PathFragment path) { |
| return new File(path.toString()); |
| } |
| |
| /** |
| * Returns a {@link java.nio.file.Path} representing the same path as provided {@code path}. |
| * |
| * <p>Note: while it's possible to use {@link #getIoFile(PathFragment)} in combination with {@link |
| * File#toPath()} to achieve essentially the same, using this method is preferable because it |
| * avoids extra allocations and does not lose track of the underlying Java filesystem, which is |
| * useful for some in-memory filesystem implementations like JimFS. |
| */ |
| protected java.nio.file.Path getNioPath(PathFragment path) { |
| return Paths.get(path.toString()); |
| } |
| |
| private LinkOption[] linkOpts(boolean followSymlinks) { |
| return followSymlinks ? NO_LINK_OPTION : NOFOLLOW_LINKS_OPTION; |
| } |
| |
| @Override |
| protected Collection<String> getDirectoryEntries(PathFragment path) throws IOException { |
| File file = getIoFile(path); |
| String[] entries = null; |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| entries = file.list(); |
| if (entries == null) { |
| if (file.exists()) { |
| throw new IOException(path + ERR_NOT_A_DIRECTORY); |
| } else { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| } |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_DIR, file.getPath()); |
| } |
| Collection<String> result = new ArrayList<>(entries.length); |
| for (String entry : entries) { |
| if (!entry.equals(".") && !entry.equals("..")) { |
| result.add(entry); |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| protected boolean exists(PathFragment path, boolean followSymlinks) { |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| java.nio.file.Path nioPath = getNioPath(path); |
| return Files.exists(nioPath, linkOpts(followSymlinks)); |
| } catch (InvalidPathException e) { |
| return false; |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path.toString()); |
| } |
| } |
| |
| @Override |
| protected boolean isReadable(PathFragment path) throws IOException { |
| File file = getIoFile(path); |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| if (!file.exists()) { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| return file.canRead(); |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath()); |
| } |
| } |
| |
| @Override |
| protected boolean isWritable(PathFragment path) throws IOException { |
| File file = getIoFile(path); |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| if (!file.exists()) { |
| if (linkExists(file)) { |
| throw new IOException(path + ERR_PERMISSION_DENIED); |
| } else { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| } |
| return file.canWrite(); |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath()); |
| } |
| } |
| |
| @Override |
| protected boolean isExecutable(PathFragment path) throws IOException { |
| File file = getIoFile(path); |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| if (!file.exists()) { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| return file.canExecute(); |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath()); |
| } |
| } |
| |
| @Override |
| protected void setReadable(PathFragment path, boolean readable) throws IOException { |
| File file = getIoFile(path); |
| if (!file.exists()) { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| if (!file.setReadable(readable) && readable) { |
| throw new IOException(String.format("Failed to make %s readable", path)); |
| } |
| } |
| |
| @Override |
| public void setWritable(PathFragment path, boolean writable) throws IOException { |
| File file = getIoFile(path); |
| if (!file.exists()) { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| if (!file.setWritable(writable) && writable) { |
| throw new IOException(String.format("Failed to make %s writable", path)); |
| } |
| } |
| |
| @Override |
| protected void setExecutable(PathFragment path, boolean executable) throws IOException { |
| File file = getIoFile(path); |
| if (!file.exists()) { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| if (!file.setExecutable(executable) && executable) { |
| throw new IOException(String.format("Failed to make %s executable", path)); |
| } |
| } |
| |
| @Override |
| public boolean supportsModifications(PathFragment path) { |
| return true; |
| } |
| |
| @Override |
| public boolean supportsSymbolicLinksNatively(PathFragment path) { |
| return true; |
| } |
| |
| @Override |
| public boolean supportsHardLinksNatively(PathFragment path) { |
| return true; |
| } |
| |
| @Override |
| public boolean isFilePathCaseSensitive() { |
| return true; |
| } |
| |
| @Override |
| public boolean createDirectory(PathFragment path) throws IOException { |
| File file = getIoFile(path); |
| if (file.mkdir()) { |
| return true; |
| } |
| |
| if (fileIsSymbolicLink(file)) { |
| throw new IOException(path + ERR_FILE_EXISTS); |
| } |
| if (file.isDirectory()) { |
| return false; // directory already existed |
| } else if (file.exists()) { |
| throw new IOException(path + ERR_FILE_EXISTS); |
| } else if (!file.getParentFile().exists()) { |
| throw new FileNotFoundException(path.getParentDirectory() + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| // Parent directory apparently exists - try to create our directory again. |
| if (file.mkdir()) { |
| return true; // Everything is fine finally. |
| } else if (!file.getParentFile().canWrite()) { |
| throw new FileAccessException(path + ERR_PERMISSION_DENIED); |
| } else { |
| // Parent exists, is writable, yet we can't create our directory. |
| throw new FileNotFoundException(path.getParentDirectory() + ERR_NOT_A_DIRECTORY); |
| } |
| } |
| |
| @Override |
| public void createDirectoryAndParents(PathFragment path) throws IOException { |
| java.nio.file.Path nioPath = getNioPath(path); |
| try { |
| Files.createDirectories(nioPath); |
| } catch (java.nio.file.FileAlreadyExistsException e) { |
| // Files.createDirectories will handle this case normally, but if the existing |
| // file is a symlink to a directory then it still throws. Swallow this. |
| if (!isDirectory(path, /*followSymlinks=*/ true)) { |
| throw e; |
| } |
| } |
| } |
| |
| private boolean linkExists(File file) { |
| String shortName = file.getName(); |
| File parentFile = file.getParentFile(); |
| if (parentFile == null) { |
| return false; |
| } |
| String[] filenames = parentFile.list(); |
| if (filenames == null) { |
| return false; |
| } |
| for (String name : filenames) { |
| if (name.equals(shortName)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| protected void createSymbolicLink(PathFragment linkPath, PathFragment targetFragment) |
| throws IOException { |
| java.nio.file.Path nioPath = getNioPath(linkPath); |
| try { |
| Files.createSymbolicLink(nioPath, Paths.get(targetFragment.getSafePathString())); |
| } catch (java.nio.file.FileAlreadyExistsException e) { |
| throw new IOException(linkPath + ERR_FILE_EXISTS, e); |
| } catch (java.nio.file.AccessDeniedException e) { |
| throw new IOException(linkPath + ERR_PERMISSION_DENIED, e); |
| } catch (java.nio.file.NoSuchFileException e) { |
| throw new FileNotFoundException(linkPath + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| } |
| |
| @Override |
| protected PathFragment readSymbolicLink(PathFragment path) throws IOException { |
| java.nio.file.Path nioPath = getNioPath(path); |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| String link = Files.readSymbolicLink(nioPath).toString(); |
| return PathFragment.create(link); |
| } catch (java.nio.file.NotLinkException e) { |
| throw new NotASymlinkException(path, e); |
| } catch (java.nio.file.NoSuchFileException e) { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_READLINK, path.getPathString()); |
| } |
| } |
| |
| @Override |
| public void renameTo(PathFragment sourcePath, PathFragment targetPath) throws IOException { |
| File sourceFile = getIoFile(sourcePath); |
| File targetFile = getIoFile(targetPath); |
| if (!sourceFile.renameTo(targetFile)) { |
| if (!sourceFile.exists()) { |
| throw new FileNotFoundException(sourcePath + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| if (targetFile.exists()) { |
| if (targetFile.isDirectory() && targetFile.list().length > 0) { |
| throw new IOException(targetPath + ERR_DIRECTORY_NOT_EMPTY); |
| } else if (sourceFile.isDirectory() && targetFile.isFile()) { |
| throw new IOException(sourcePath + " -> " + targetPath + ERR_NOT_A_DIRECTORY); |
| } else if (sourceFile.isFile() && targetFile.isDirectory()) { |
| throw new IOException(sourcePath + " -> " + targetPath + ERR_IS_DIRECTORY); |
| } else { |
| throw new IOException(sourcePath + " -> " + targetPath + ERR_PERMISSION_DENIED); |
| } |
| } else { |
| throw new FileAccessException(sourcePath + " -> " + targetPath + ERR_PERMISSION_DENIED); |
| } |
| } |
| } |
| |
| @Override |
| protected long getFileSize(PathFragment path, boolean followSymlinks) throws IOException { |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| return stat(path, followSymlinks).getSize(); |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, path.getPathString()); |
| } |
| } |
| |
| @Override |
| protected boolean delete(PathFragment path) throws IOException { |
| java.nio.file.Path nioPath = getNioPath(path); |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| return Files.deleteIfExists(nioPath); |
| } catch (java.nio.file.DirectoryNotEmptyException e) { |
| throw new IOException(path.getPathString() + ERR_DIRECTORY_NOT_EMPTY, e); |
| } catch (java.nio.file.AccessDeniedException e) { |
| throw new IOException(path.getPathString() + ERR_PERMISSION_DENIED, e); |
| } catch (java.nio.file.AtomicMoveNotSupportedException |
| | java.nio.file.FileAlreadyExistsException |
| | java.nio.file.FileSystemLoopException |
| | java.nio.file.NoSuchFileException |
| | java.nio.file.NotDirectoryException |
| | java.nio.file.NotLinkException e) { |
| // All known but unexpected subclasses of FileSystemException. |
| throw new IOException(path.getPathString() + ": unexpected FileSystemException", e); |
| } catch (java.nio.file.FileSystemException e) { |
| // Files.deleteIfExists() throws FileSystemException on Linux if a path component is a file. |
| // We caught all known subclasses of FileSystemException so `e` is either an unknown |
| // subclass or it is indeed a "Not a directory" error. Non-English JDKs may use a different |
| // error message than "Not a directory", so we should not look for that text. Checking the |
| // parent directory if it's indeed a directory is unrealiable, because another process may |
| // modify it concurrently... but we have no better choice. |
| if (e.getClass().equals(java.nio.file.FileSystemException.class) |
| && !nioPath.getParent().toFile().isDirectory()) { |
| // Hopefully the try-block failed because a parent directory was in fact not a directory. |
| // Theoretically it's possible that the try-block failed for some other reason and all |
| // parent directories were indeed directories, but another process changed a parent |
| // directory into a file after the try-block failed but before this catch-block started, and |
| // we return false here losing the real exception in `e`, but we cannot know. |
| return false; |
| } else { |
| throw new IOException(path.getPathString() + ": unexpected FileSystemException", e); |
| } |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_DELETE, path.getPathString()); |
| } |
| } |
| |
| @Override |
| protected long getLastModifiedTime(PathFragment path, boolean followSymlinks) throws IOException { |
| File file = getIoFile(path); |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| return stat(path, followSymlinks).getLastModifiedTime(); |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_STAT, file.getPath()); |
| } |
| } |
| |
| protected boolean fileIsSymbolicLink(File file) { |
| return Files.isSymbolicLink(file.toPath()); |
| } |
| |
| @Override |
| public void setLastModifiedTime(PathFragment path, long newTime) throws IOException { |
| File file = getIoFile(path); |
| if (!file.setLastModified( |
| newTime == Path.NOW_SENTINEL_TIME ? clock.currentTimeMillis() : newTime)) { |
| if (!file.exists()) { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } else if (!file.getParentFile().canWrite()) { |
| throw new FileAccessException(path.getParentDirectory() + ERR_PERMISSION_DENIED); |
| } else { |
| throw new FileAccessException(path + ERR_PERMISSION_DENIED); |
| } |
| } |
| } |
| |
| @Override |
| protected byte[] getDigest(PathFragment path) throws IOException { |
| String name = path.toString(); |
| long startTime = Profiler.nanoTimeMaybe(); |
| try { |
| return super.getDigest(path); |
| } finally { |
| profiler.logSimpleTask(startTime, ProfilerTask.VFS_MD5, name); |
| } |
| } |
| |
| /** |
| * Returns the status of a file. See {@link Path#stat(Symlinks)} for specification. |
| * |
| * <p>The default implementation of this method is a "lazy" one, based on other accessor methods |
| * such as {@link #isFile}, etc. Subclasses may provide more efficient specializations. However, |
| * we still try to follow Unix-like semantics of failing fast in case of non-existent files (or in |
| * case of permission issues). |
| */ |
| @Override |
| protected FileStatus stat(PathFragment path, boolean followSymlinks) throws IOException { |
| java.nio.file.Path nioPath = getNioPath(path); |
| final BasicFileAttributes attributes; |
| try { |
| attributes = |
| Files.readAttributes(nioPath, BasicFileAttributes.class, linkOpts(followSymlinks)); |
| } catch (java.nio.file.FileSystemException e) { |
| throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR); |
| } |
| FileStatus status = |
| new FileStatus() { |
| @Override |
| public boolean isFile() { |
| return attributes.isRegularFile() || isSpecialFile(); |
| } |
| |
| @Override |
| public boolean isSpecialFile() { |
| return attributes.isOther(); |
| } |
| |
| @Override |
| public boolean isDirectory() { |
| return attributes.isDirectory(); |
| } |
| |
| @Override |
| public boolean isSymbolicLink() { |
| return attributes.isSymbolicLink(); |
| } |
| |
| @Override |
| public long getSize() { |
| return attributes.size(); |
| } |
| |
| @Override |
| public long getLastModifiedTime() { |
| return attributes.lastModifiedTime().toMillis(); |
| } |
| |
| @Override |
| public long getLastChangeTime() { |
| // This is the best we can do with Java NIO... |
| return attributes.lastModifiedTime().toMillis(); |
| } |
| |
| @Override |
| public long getNodeId() { |
| // TODO(bazel-team): Consider making use of attributes.fileKey(). |
| return -1; |
| } |
| }; |
| |
| return status; |
| } |
| |
| @Override |
| @Nullable |
| protected FileStatus statIfFound(PathFragment path, boolean followSymlinks) { |
| try { |
| return stat(path, followSymlinks); |
| } catch (FileNotFoundException e) { |
| // JavaIoFileSystem#stat (incorrectly) only throws FileNotFoundException (because it calls |
| // #getLastModifiedTime, which can only throw a FileNotFoundException), so we always hit this |
| // codepath. Thus, this method will incorrectly not throw an exception for some filesystem |
| // errors. |
| return null; |
| } catch (IOException e) { |
| // If this codepath is ever hit, then this method should be rewritten to properly distinguish |
| // between not-found exceptions and others. |
| throw new IllegalStateException(e); |
| } |
| } |
| |
| @Override |
| protected void createFSDependentHardLink(PathFragment linkPath, PathFragment originalPath) |
| throws IOException { |
| Files.createLink( |
| java.nio.file.Paths.get(linkPath.toString()), |
| java.nio.file.Paths.get(originalPath.toString())); |
| } |
| } |