diff options
author | Han-Wen Nienhuys <hanwen@google.com> | 2015-02-25 16:45:20 +0100 |
---|---|---|
committer | Han-Wen Nienhuys <hanwen@google.com> | 2015-02-25 16:45:20 +0100 |
commit | d08b27fa9701fecfdb69e1b0d1ac2459efc2129b (patch) | |
tree | 5d50963026239ca5aebfb47ea5b8db7e814e57c8 /src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java |
Update from Google.
--
MOE_MIGRATED_REVID=85702957
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java')
-rw-r--r-- | src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java new file mode 100644 index 0000000000..9d4160981c --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java @@ -0,0 +1,632 @@ +// Copyright 2014 Google Inc. 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 static java.nio.charset.StandardCharsets.ISO_8859_1; + +import com.google.common.collect.Lists; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteSource; +import com.google.common.io.CharStreams; +import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; +import com.google.devtools.build.lib.vfs.Dirent.Type; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Collection; +import java.util.List; + +/** + * This interface models a file system using UNIX the naming scheme. + */ +@ThreadSafe +public abstract class FileSystem { + + /** + * An exception thrown when attempting to resolve an ordinary file as a symlink. + */ + protected static final class NotASymlinkException extends IOException { + public NotASymlinkException(Path path) { + super(path.toString()); + } + } + + protected final Path rootPath; + + protected FileSystem() { + this.rootPath = createRootPath(); + } + + /** + * Creates the root of all paths used by this filesystem. This is a hook + * allowing subclasses to define their own root path class. All other paths + * are created via the root path's {@link Path#createChildPath(String)} method. + * <p> + * Beware: this is called during the FileSystem constructor which may occur + * before subclasses are completely initialized. + */ + protected Path createRootPath() { + return new Path(this); + } + + /** + * Returns an absolute path instance, given an absolute path name, without + * double slashes, .., or . segments. While this method will normalize the + * path representation by creating a structured/parsed representation, it will + * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix + * file system. + */ + public Path getPath(String pathName) { + return getPath(new PathFragment(pathName)); + } + + /** + * Returns an absolute path instance, given an absolute path name, without + * double slashes, .., or . segments. While this method will normalize the + * path representation by creating a structured/parsed representation, it will + * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix + * file system. + */ + public Path getPath(PathFragment pathName) { + if (!pathName.isAbsolute()) { + throw new IllegalArgumentException(pathName.getPathString() + " (not an absolute path)"); + } + return rootPath.getRelative(pathName); + } + + /** + * Returns a path representing the root directory of the current file system. + */ + public final Path getRootDirectory() { + return rootPath; + } + + /** + * Returns whether or not the FileSystem supports modifications of files and + * file entries. + * + * <p>Returns true if FileSystem supports the following: + * <ul> + * <li>{@link #setWritable(Path, boolean)}</li> + * <li>{@link #setExecutable(Path, boolean)}</li> + * </ul> + * + * The above calls will result in an {@link UnsupportedOperationException} on + * a FileSystem where this method returns {@code false}. + */ + public abstract boolean supportsModifications(); + + /** + * Returns whether or not the FileSystem supports symbolic links. + * + * <p>Returns true if FileSystem supports the following: + * <ul> + * <li>{@link #createSymbolicLink(Path, PathFragment)}</li> + * <li>{@link #getFileSize(Path, boolean)} where {@code followSymlinks=false}</li> + * <li>{@link #getLastModifiedTime(Path, boolean)} where {@code followSymlinks=false}</li> + * <li>{@link #readSymbolicLink(Path)} where the link points to a non-existent file</li> + * </ul> + * + * The above calls will result in an {@link UnsupportedOperationException} on + * a FileSystem where this method returns {@code false}. + */ + public abstract boolean supportsSymbolicLinks(); + + /** + * Returns the type of the file system path belongs to. + * + * <p>The string returned is obtained directly from the operating system, so + * it's a best guess in absence of a guaranteed api. + * + * <p>This implementation uses <code>/proc/mounts</code> to determine the + * file system type. + */ + public String getFileSystemType(Path path) { + String fileSystem = "unknown"; + int bestMountPointSegmentCount = -1; + try { + Path canonicalPath = path.resolveSymbolicLinks(); + Path mountTable = path.getRelative("/proc/mounts"); + for (String line : CharStreams.readLines(new InputStreamReader(mountTable.getInputStream(), + ISO_8859_1))) { + String[] words = line.split("\\s+"); + if (words.length >= 3) { + if (!words[1].startsWith("/")) { + continue; + } + Path mountPoint = path.getFileSystem().getPath(words[1]); + int segmentCount = mountPoint.asFragment().segmentCount(); + if (canonicalPath.startsWith(mountPoint) && segmentCount > bestMountPointSegmentCount) { + bestMountPointSegmentCount = segmentCount; + fileSystem = words[2]; + } + } + } + } catch (IOException e) { + // pass + } + return fileSystem; + } + + + /** + * Creates a directory with the name of the current path. See + * {@link Path#createDirectory} for specification. + */ + protected abstract boolean createDirectory(Path path) throws IOException; + + /** + * Returns the size in bytes of the file denoted by {@code path}. See + * {@link Path#getFileSize(Symlinks)} for specification. + * + * <p>Note: for <@link FileSystem>s where {@link #supportsSymbolicLinks()} + * returns false, this method will throw an + * {@link UnsupportedOperationException} if {@code followSymLinks=false}. + */ + protected abstract long getFileSize(Path path, boolean followSymlinks) throws IOException; + + /** + * Deletes the file denoted by {@code path}. See {@link Path#delete} for + * specification. + */ + protected abstract boolean delete(Path path) throws IOException; + + /** + * Returns the last modification time of the file denoted by {@code path}. + * See {@link Path#getLastModifiedTime(Symlinks)} for specification. + * + * Note: for {@link FileSystem}s where {@link #supportsSymbolicLinks()} returns + * false, this method will throw an {@link UnsupportedOperationException} if + * {@code followSymLinks=false}. + */ + protected abstract long getLastModifiedTime(Path path, + boolean followSymlinks) + throws IOException; + + /** + * Sets the last modification time of the file denoted by {@code path}. See + * {@link Path#setLastModifiedTime} for specification. + */ + protected abstract void setLastModifiedTime(Path path, long newTime) throws IOException; + + /** + * Returns value of the given extended attribute name or null if attribute + * does not exist or file system does not support extended attributes. + * <p>Default implementation assumes that file system does not support + * extended attributes and always returns null. Specific file system + * implementations should override this method if they do provide support + * for extended attributes. + * + * @param path the file whose extended attribute is to be returned. + * @param name the name of the extended attribute key. + * @return the value of the extended attribute associated with 'path', if + * any, or null if no such attribute is defined (ENODATA) or file + * system does not support extended attributes at all. + * @throws IOException if the call failed for any other reason. + */ + protected byte[] getxattr(Path path, String name, boolean followSymlinks) throws IOException { + return null; + } + + /** + * Returns the type of digest that may be returned by {@link #getFastDigest}, or {@code null} + * if the filesystem doesn't support them. + */ + protected String getFastDigestFunctionType(Path path) { + return null; + } + + /** + * Gets a fast digest for the given path, or {@code null} if there isn't one available or the + * filesystem doesn't support them. This digest should be suitable for detecting changes to the + * file. + */ + protected byte[] getFastDigest(Path path) throws IOException { + return null; + } + + /** + * Returns the MD5 digest of the file denoted by {@code path}. See + * {@link Path#getMD5Digest} for specification. + */ + protected byte[] getMD5Digest(final Path path) throws IOException { + // Naive I/O implementation. Subclasses may (and do) optimize. + // This code is only used by the InMemory or Zip or other weird FSs. + return new ByteSource() { + @Override + public InputStream openStream() throws IOException { + return getInputStream(path); + } + }.hash(Hashing.md5()).asBytes(); + } + + /** + * Returns true if "path" denotes an existing symbolic link. See + * {@link Path#isSymbolicLink} for specification. + */ + protected abstract boolean isSymbolicLink(Path path); + + /** + * Appends a single regular path segment 'child' to 'dir', recursively + * resolving symbolic links in 'child'. 'dir' must be canonical. 'maxLinks' is + * the maximum number of symbolic links that may be traversed before it gives + * up (the Linux kernel uses 32). + * + * <p>(This method does not need to be synchronized; but the result may be + * stale in the case of concurrent modification.) + * + * @throws IOException if 'dir' is not an existing directory; or if + * stat(child) fails for any reason, or if 'child' is a symlink and + * readlink(child) fails for any reason (e.g. ENOENT, EACCES), or if + * the chain of symbolic links exceeds 'maxLinks'. + */ + private Path appendSegment(Path dir, String child, int maxLinks) throws IOException { + Path naive = dir.getChild(child); + + PathFragment linkTarget = resolveOneLink(naive); + if (linkTarget == null) { + return naive; // regular file or directory + } + + if (maxLinks-- == 0) { + throw new IOException(naive + " (Too many levels of symbolic links)"); + } + if (linkTarget.isAbsolute()) { dir = rootPath; } + for (String name : linkTarget.segments()) { + if (name.equals(".") || name.equals("")) { + // no-op + } else if (name.equals("..")) { + Path parent = dir.getParentDirectory(); + // root's parent is root, when canonicalizing, so this is a no-op. + if (parent != null) { dir = parent; } + } else { + dir = appendSegment(dir, name, maxLinks); + } + } + return dir; + } + + /** + * Helper method of {@link #resolveSymbolicLinks(Path)}. This method + * encapsulates the I/O component of a full canonicalization operation. + * Subclasses can (and do) provide more efficient implementations. + * + * <p>(This method does not need to be synchronized; but the result may be + * stale in the case of concurrent modification.) + * + * @param path a path, of which all but the last segment is guaranteed to be + * canonical + * @return {@link #readSymbolicLink} iff path is a symlink or null iff + * path exists but is not a symlink + * @throws IOException if the file did not exist, or a parent directory could + * not be searched + */ + protected PathFragment resolveOneLink(Path path) throws IOException { + try { + return readSymbolicLink(path); + } catch (NotASymlinkException e) { + // Not a symbolic link. Check it exists. + + // (A simple call to lstat would replace all of this.) + if (!exists(path, false)) { + throw new FileNotFoundException(path + " (No such file or directory)"); + } + + // TODO(bazel-team): (2009) ideally, throw ENOTDIR if dir is not a dir, but that + // would require twice as many stats, or a much more convoluted + // implementation (like glibc's canonicalize.c). + + return null; // exists. + } + } + + /** + * Returns the canonical path for the given path. See + * {@link Path#resolveSymbolicLinks} for specification. + */ + protected final Path resolveSymbolicLinks(Path path) + throws IOException { + Path parentNode = path.getParentDirectory(); + return parentNode == null + ? path // (root) + : appendSegment(resolveSymbolicLinks(parentNode), path.getBaseName(), 32); + } + + /** + * 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). + */ + protected FileStatus stat(final Path path, final boolean followSymlinks) throws IOException { + FileStatus status = new FileStatus() { + volatile Boolean isFile; + volatile Boolean isDirectory; + volatile Boolean isSymbolicLink; + volatile long size = -1; + volatile long mtime = -1; + + @Override + public boolean isFile() { + if (isFile == null) { isFile = FileSystem.this.isFile(path, followSymlinks); } + return isFile; + } + + @Override + public boolean isDirectory() { + if (isDirectory == null) { + isDirectory = FileSystem.this.isDirectory(path, followSymlinks); + } + return isDirectory; + } + + @Override + public boolean isSymbolicLink() { + if (isSymbolicLink == null) { isSymbolicLink = FileSystem.this.isSymbolicLink(path); } + return isSymbolicLink; + } + + @Override + public long getSize() throws IOException { + if (size == -1) { size = getFileSize(path, followSymlinks); } + return size; + } + + @Override + public long getLastModifiedTime() throws IOException { + if (mtime == -1) { mtime = FileSystem.this.getLastModifiedTime(path, followSymlinks); } + return mtime; + } + + @Override + public long getLastChangeTime() { + throw new UnsupportedOperationException(); + } + + @Override + public long getNodeId() { + throw new UnsupportedOperationException(); + } + }; + + // Fail fast in case if some operations will actually fail, since stat() call sometimes used + // to verify file existence as well. We will use getLastModifiedTime() method for that purpose. + status.getLastModifiedTime(); + + return status; + } + + /** + * Like stat(), but returns null on failures instead of throwing. + */ + protected FileStatus statNullable(Path path, boolean followSymlinks) { + try { + return stat(path, followSymlinks); + } catch (IOException e) { + return null; + } + } + + /** + * Like {@link #stat}, but returns null if the file is not found (corresponding to + * {@code ENOENT} or {@code ENOTDIR} in Unix's stat(2) function) instead of throwing. Note that + * this implementation does <i>not</i> successfully catch {@code ENOTDIR} exceptions. If the + * instantiated filesystem can catch such errors, it should override this method to do so. + */ + protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException { + try { + return stat(path, followSymlinks); + } catch (FileNotFoundException e) { + return null; + } + } + + /** + * Returns true iff {@code path} denotes an existing directory. See + * {@link Path#isDirectory(Symlinks)} for specification. + */ + protected abstract boolean isDirectory(Path path, boolean followSymlinks); + + /** + * Returns true iff {@code path} denotes an existing regular or special file. + * See {@link Path#isFile(Symlinks)} for specification. + */ + protected abstract boolean isFile(Path path, boolean followSymlinks); + + /** + * Creates a symbolic link. See {@link Path#createSymbolicLink(Path)} for + * specification. + * + * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinks()} + * returns false, this method will throw an + * {@link UnsupportedOperationException} + */ + protected abstract void createSymbolicLink(Path linkPath, PathFragment targetFragment) + throws IOException; + + /** + * Returns the target of a symbolic link. See {@link Path#readSymbolicLink} + * for specification. + * + * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinks()} + * returns false, this method will throw an + * {@link UnsupportedOperationException} if the link points to a non-existent + * file. + * + * @throws NotASymlinkException if the current path is not a symbolic link + * @throws IOException if the contents of the link could not be read for any reason. + */ + protected abstract PathFragment readSymbolicLink(Path path) throws IOException; + + /** + * Returns true iff {@code path} denotes an existing file of any kind. See + * {@link Path#exists(Symlinks)} for specification. + */ + protected abstract boolean exists(Path path, boolean followSymlinks); + + /** + * Returns a collection containing the names of all entities within the + * directory denoted by the {@code path}. + * + * @throws IOException if there was an error reading the directory entries + */ + protected abstract Collection<Path> getDirectoryEntries(Path path) throws IOException; + + /** + * Returns a Dirents structure, listing the names of all entries within the + * directory {@code path}, plus their types (file, directory, other). + * + * @param followSymlinks whether to follow symlinks when determining the file types of + * individual directory entries. No matter the value of this parameter, symlinks are + * followed when resolving the directory whose entries are to be read. + * @throws IOException if there was an error reading the directory entries + */ + protected Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException { + Collection<Path> children = getDirectoryEntries(path); + List<Dirent> dirents = Lists.newArrayListWithCapacity(children.size()); + for (Path child : children) { + FileStatus stat = statNullable(child, followSymlinks); + Dirent.Type type; + if (stat == null) { + type = Type.UNKNOWN; + } else if (stat.isFile()) { + type = Type.FILE; + } else if (stat.isDirectory()) { + type = Type.DIRECTORY; + } else if (stat.isSymbolicLink()) { + type = Type.SYMLINK; + } else { + type = Type.UNKNOWN; + } + dirents.add(new Dirent(child.getBaseName(), type)); + } + return dirents; + } + + /** + * Returns true iff the file represented by {@code path} is readable. + * + * @throws IOException if there was an error reading the file's metadata + */ + protected abstract boolean isReadable(Path path) throws IOException; + + /** + * Sets the file to readable (if the argument is true) or non-readable (if the + * argument is false) + * + * <p>Note: for {@link FileSystem}s where {@link #supportsModifications()} + * returns false or which do not support unreadable files, this method will + * throw an {@link UnsupportedOperationException}. + * + * @throws IOException if there was an error reading or writing the file's metadata + */ + protected abstract void setReadable(Path path, boolean readable) + throws IOException; + + /** + * Returns true iff the file represented by {@code path} is writable. + * + * @throws IOException if there was an error reading the file's metadata + */ + protected abstract boolean isWritable(Path path) throws IOException; + + /** + * Sets the file to writable (if the argument is true) or non-writable (if the + * argument is false) + * + * <p>Note: for {@link FileSystem}s where {@link #supportsModifications()} + * returns false, this method will throw an + * {@link UnsupportedOperationException}. + * + * @throws IOException if there was an error reading or writing the file's metadata + */ + protected abstract void setWritable(Path path, boolean writable) + throws IOException; + + /** + * Returns true iff the file represented by the path is executable. + * + * @throws IOException if there was an error reading the file's metadata + */ + protected abstract boolean isExecutable(Path path) throws IOException; + + /** + * Sets the file to executable, if the argument is true. It is currently not + * supported to unset the executable status of a file, so {code + * executable=false} yields an {@link UnsupportedOperationException}. + * + * <p>Note: for {@link FileSystem}s where {@link #supportsModifications()} + * returns false, this method will throw an + * {@link UnsupportedOperationException}. + * + * @throws IOException if there was an error reading or writing the file's metadata + */ + protected abstract void setExecutable(Path path, boolean executable) throws IOException; + + /** + * Sets the file permissions. If permission changes on this {@link FileSystem} + * are slow (e.g. one syscall per change), this method should aim to be faster + * than setting each permission individually. If this {@link FileSystem} does + * not support group or others permissions, those bits will be ignored. + * + * <p>Note: for {@link FileSystem}s where {@link #supportsModifications()} + * returns false, this method will throw an + * {@link UnsupportedOperationException}. + * + * @throws IOException if there was an error reading or writing the file's metadata + */ + protected void chmod(Path path, int mode) throws IOException { + setReadable(path, (mode & 0400) != 0); + setWritable(path, (mode & 0200) != 0); + setExecutable(path, (mode & 0100) != 0); + } + + /** + * Creates an InputStream accessing the file denoted by the path. + * + * @throws IOException if there was an error opening the file for reading + */ + protected abstract InputStream getInputStream(Path path) throws IOException; + + /** + * Creates an OutputStream accessing the file denoted by path. + * + * @throws IOException if there was an error opening the file for writing + */ + protected final OutputStream getOutputStream(Path path) throws IOException { + return getOutputStream(path, false); + } + + /** + * Creates an OutputStream accessing the file denoted by path. + * + * @param append whether to open the output stream in append mode + * @throws IOException if there was an error opening the file for writing + */ + protected abstract OutputStream getOutputStream(Path path, boolean append) throws IOException; + + /** + * Renames the file denoted by "sourceNode" to the location "targetNode". + * See {@link Path#renameTo} for specification. + */ + protected abstract void renameTo(Path sourcePath, Path targetPath) throws IOException; +} |