blob: 01ff5aa838ce56bc939ae57611324835c14bea15 [file] [log] [blame]
// 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.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Presents a unified view of multiple virtual {@link FileSystem} instances, to which requests are
* delegated based on a {@link PathFragment} prefix mapping. If multiple prefixes apply to a given
* path, the *longest* (i.e. most specific) match is used. The order in which the delegates are
* specified does not influence the mapping.
*
* <p>Paths are preserved absolutely, contrary to how "mount" works, e.g.: /foo/bar maps to /foo/bar
* on the delegate, even if it is mounted at /foo.
*
* <p>For example: "/in" maps to InFileSystem, "/" maps to OtherFileSystem. Reading from
* "/in/base/BUILD" through the UnionFileSystem will delegate the read operation to InFileSystem,
* which will read "/in/base/BUILD" relative to its root. ("mount" behavior would remap it to
* "/base/BUILD" on the delegate).
*
* <p>Intra-filesystem symbolic links are resolved to their ultimate targets. Cross-filesystem links
* are not currently supported.
*/
@ThreadSafety.ThreadSafe
public class UnionFileSystem extends FileSystem {
// Prefix trie index, allowing children to easily inherit prefix mappings
// of their parents.
// This does not currently handle unicode filenames.
private final PathTrie<FileSystem> pathDelegate;
// True if the file path is case-sensitive on all the FileSystem
// or False if they are all case-insensitive, otherwise error.
private final boolean isCaseSensitive;
/**
* Creates a new modifiable UnionFileSystem with prefix mappings specified by a map.
*
* @param prefixMapping map of path prefixes to {@link FileSystem}s
* @param rootFileSystem root for default requests; i.e. mapping of "/"
*/
public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping, FileSystem rootFileSystem) {
super();
Preconditions.checkNotNull(prefixMapping);
Preconditions.checkNotNull(rootFileSystem);
Preconditions.checkArgument(rootFileSystem != this, "Circular root filesystem.");
Preconditions.checkArgument(
!prefixMapping.containsKey(PathFragment.EMPTY_FRAGMENT),
"Attempted to specify an explicit root prefix mapping; "
+ "please use the rootFileSystem argument instead.");
this.pathDelegate = new PathTrie<>();
this.isCaseSensitive = rootFileSystem.isFilePathCaseSensitive();
for (Map.Entry<PathFragment, FileSystem> prefix : prefixMapping.entrySet()) {
FileSystem delegate = prefix.getValue();
Preconditions.checkArgument(
delegate.isFilePathCaseSensitive() == this.isCaseSensitive,
"The case sensitiveness of FileSystem are different in UnionFileSystem");
PathFragment prefixPath = prefix.getKey();
// Extra slash prevents within-directory mappings, which Path can't handle.
pathDelegate.put(prefixPath, delegate);
}
pathDelegate.put(PathFragment.ROOT_FRAGMENT, rootFileSystem);
}
/**
* Retrieves the filesystem delegate of a path mapping. Does not follow symlinks (but you can call
* on a path preprocessed with {@link #resolveSymbolicLinks} to support this use case).
*
* @param path the {@link Path} to map to a filesystem
* @throws IllegalArgumentException if no delegate exists for the path
*/
protected FileSystem getDelegate(Path path) {
Preconditions.checkNotNull(path);
FileSystem immediateDelegate = pathDelegate.get(path.asFragment());
// Should never actually happen if the root delegate is present.
Preconditions.checkNotNull(immediateDelegate, "No delegate filesystem exists for %s", path);
return immediateDelegate;
}
// Associates the path with the root of the given delegate filesystem.
// Necessary to avoid null pointer problems inside of the delegates.
protected Path adjustPath(Path path, FileSystem delegate) {
return delegate.getPath(path.asFragment());
}
/**
* Follow a symbolic link once using the appropriate delegate filesystem, also resolving parent
* directory symlinks.
*
* @param path {@link Path} to the symbolic link
*/
@Override
protected PathFragment readSymbolicLink(Path path) throws IOException {
Preconditions.checkNotNull(path);
FileSystem delegate = getDelegate(path);
return delegate.readSymbolicLink(adjustPath(path, delegate));
}
@Override
protected PathFragment resolveOneLink(Path path) throws IOException {
Preconditions.checkNotNull(path);
FileSystem delegate = getDelegate(path);
return delegate.resolveOneLink(adjustPath(path, delegate));
}
private void checkModifiable(Path path) {
if (!supportsModifications(path)) {
throw new UnsupportedOperationException(
String.format("Modifications to this %s are disabled.", getClass().getSimpleName()));
}
}
@Override
public boolean supportsModifications(Path path) {
FileSystem delegate = getDelegate(path);
path = adjustPath(path, delegate);
return delegate.supportsModifications(path);
}
@Override
public boolean supportsSymbolicLinksNatively(Path path) {
FileSystem delegate = getDelegate(path);
path = adjustPath(path, delegate);
return delegate.supportsSymbolicLinksNatively(path);
}
@Override
public boolean supportsHardLinksNatively(Path path) {
FileSystem delegate = getDelegate(path);
path = adjustPath(path, delegate);
return delegate.supportsHardLinksNatively(path);
}
@Override
public boolean isFilePathCaseSensitive() {
return isCaseSensitive;
}
@Override
public String getFileSystemType(Path path) {
try {
path = internalResolveSymlink(path);
} catch (IOException e) {
return "unknown";
}
FileSystem delegate = getDelegate(path);
return delegate.getFileSystemType(path);
}
@Override
protected byte[] getDigest(Path path, HashFunction hashFunction) throws IOException {
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
return delegate.getDigest(adjustPath(path, delegate), hashFunction);
}
@Override
protected boolean createDirectory(Path path) throws IOException {
checkModifiable(path);
// When creating the exact directory that is mapped,
// create it on both the parent's delegate and the path's delegate.
// This is necessary both for the parent to see the directory and for the
// delegate to use it.
// This is present to address this problematic case:
// / -> RootFs
// /foo -> FooFs
// mkdir /foo
// ls / ("foo" would be missing if not created on the parent)
// ls /foo (would fail if foo weren't also present on the child)
FileSystem delegate = getDelegate(path);
Path parent = path.getParentDirectory();
if (parent != null) {
parent = internalResolveSymlink(parent);
FileSystem parentDelegate = getDelegate(parent);
if (parentDelegate != delegate) {
// There's a possibility it already exists on the parent, so don't die
// if the directory can't be created there.
parentDelegate.createDirectory(adjustPath(path, parentDelegate));
}
}
return delegate.createDirectory(adjustPath(path, delegate));
}
@Override
protected long getFileSize(Path path, boolean followSymlinks) throws IOException {
path = followSymlinks ? internalResolveSymlink(path) : path;
FileSystem delegate = getDelegate(path);
return delegate.getFileSize(adjustPath(path, delegate), false);
}
@Override
protected boolean delete(Path path) throws IOException {
checkModifiable(path);
FileSystem delegate = getDelegate(path);
return delegate.delete(adjustPath(path, delegate));
}
@Override
protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException {
path = followSymlinks ? internalResolveSymlink(path) : path;
FileSystem delegate = getDelegate(path);
return delegate.getLastModifiedTime(adjustPath(path, delegate), false);
}
@Override
protected void setLastModifiedTime(Path path, long newTime) throws IOException {
path = internalResolveSymlink(path);
checkModifiable(path);
FileSystem delegate = getDelegate(path);
delegate.setLastModifiedTime(adjustPath(path, delegate), newTime);
}
@Override
protected boolean isSymbolicLink(Path path) {
FileSystem delegate = getDelegate(path);
path = adjustPath(path, delegate);
return delegate.isSymbolicLink(path);
}
@Override
protected boolean isDirectory(Path path, boolean followSymlinks) {
try {
path = followSymlinks ? internalResolveSymlink(path) : path;
} catch (IOException e) {
return false;
}
FileSystem delegate = getDelegate(path);
return delegate.isDirectory(adjustPath(path, delegate), false);
}
@Override
protected boolean isFile(Path path, boolean followSymlinks) {
try {
path = followSymlinks ? internalResolveSymlink(path) : path;
} catch (IOException e) {
return false;
}
FileSystem delegate = getDelegate(path);
return delegate.isFile(adjustPath(path, delegate), false);
}
@Override
protected boolean isSpecialFile(Path path, boolean followSymlinks) {
try {
path = followSymlinks ? internalResolveSymlink(path) : path;
} catch (IOException e) {
return false;
}
FileSystem delegate = getDelegate(path);
return delegate.isSpecialFile(adjustPath(path, delegate), false);
}
@Override
protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException {
checkModifiable(linkPath);
if (!supportsSymbolicLinksNatively(linkPath)) {
throw new UnsupportedOperationException(
"Attempted to create a symlink, but symlink support is disabled.");
}
FileSystem delegate = getDelegate(linkPath);
delegate.createSymbolicLink(adjustPath(linkPath, delegate), targetFragment);
}
@Override
protected boolean exists(Path path, boolean followSymlinks) {
try {
path = followSymlinks ? internalResolveSymlink(path) : path;
} catch (IOException e) {
return false;
}
FileSystem delegate = getDelegate(path);
return delegate.exists(adjustPath(path, delegate), false);
}
@Override
protected FileStatus stat(Path path, boolean followSymlinks) throws IOException {
path = followSymlinks ? internalResolveSymlink(path) : path;
FileSystem delegate = getDelegate(path);
return delegate.stat(adjustPath(path, delegate), false);
}
// Needs to be overridden for the delegation logic, because the
// UnixFileSystem implements statNullable and stat as separate codepaths.
// More generally, we wish to delegate all filesystem operations.
@Override
protected FileStatus statNullable(Path path, boolean followSymlinks) {
try {
path = followSymlinks ? internalResolveSymlink(path) : path;
} catch (IOException e) {
return null;
}
FileSystem delegate = getDelegate(path);
return delegate.statNullable(adjustPath(path, delegate), false);
}
@Override
@Nullable
protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
path = followSymlinks ? internalResolveSymlink(path) : path;
FileSystem delegate = getDelegate(path);
return delegate.statIfFound(adjustPath(path, delegate), false);
}
/**
* Retrieves the directory entries for the specified path under the assumption that {@code
* resolvedPath} is the resolved path of {@code path} in one of the underlying file systems.
*
* @param path the {@link Path} whose children are to be retrieved
*/
@Override
protected Collection<String> getDirectoryEntries(Path path) throws IOException {
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
Path resolvedPath = adjustPath(path, delegate);
Collection<Path> entries = resolvedPath.getDirectoryEntries();
Collection<String> result = Lists.newArrayListWithCapacity(entries.size());
for (Path entry : entries) {
result.add(entry.getBaseName());
}
return result;
}
// No need for the more complex logic of getDirectoryEntries; it calls it implicitly.
@Override
protected Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
path = followSymlinks ? internalResolveSymlink(path) : path;
FileSystem delegate = getDelegate(path);
return delegate.readdir(adjustPath(path, delegate), false);
}
@Override
protected boolean isReadable(Path path) throws IOException {
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
return delegate.isReadable(adjustPath(path, delegate));
}
@Override
protected void setReadable(Path path, boolean readable) throws IOException {
path = internalResolveSymlink(path);
checkModifiable(path);
FileSystem delegate = getDelegate(path);
delegate.setReadable(adjustPath(path, delegate), readable);
}
@Override
protected boolean isWritable(Path path) throws IOException {
if (!supportsModifications(path)) {
return false;
}
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
return delegate.isWritable(adjustPath(path, delegate));
}
@Override
protected void setWritable(Path path, boolean writable) throws IOException {
checkModifiable(path);
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
delegate.setWritable(adjustPath(path, delegate), writable);
}
@Override
protected boolean isExecutable(Path path) throws IOException {
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
return delegate.isExecutable(adjustPath(path, delegate));
}
@Override
protected void setExecutable(Path path, boolean executable) throws IOException {
path = internalResolveSymlink(path);
checkModifiable(path);
FileSystem delegate = getDelegate(path);
delegate.setExecutable(adjustPath(path, delegate), executable);
}
@Override
protected byte[] getFastDigest(Path path, HashFunction hashFunction) throws IOException {
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
return delegate.getFastDigest(adjustPath(path, delegate), hashFunction);
}
@Override
protected byte[] getxattr(Path path, String name) throws IOException {
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
return delegate.getxattr(adjustPath(path, delegate), name);
}
@Override
protected InputStream getInputStream(Path path) throws IOException {
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
return delegate.getInputStream(adjustPath(path, delegate));
}
@Override
protected OutputStream getOutputStream(Path path, boolean append) throws IOException {
path = internalResolveSymlink(path);
checkModifiable(path);
FileSystem delegate = getDelegate(path);
return delegate.getOutputStream(adjustPath(path, delegate), append);
}
@Override
protected void renameTo(Path sourcePath, Path targetPath) throws IOException {
sourcePath = internalResolveSymlink(sourcePath);
FileSystem sourceDelegate = getDelegate(sourcePath);
if (!sourceDelegate.supportsModifications(sourcePath)) {
throw new UnsupportedOperationException(
String.format(
"The filesystem for the source path %s does not support modifications.",
sourcePath.getPathString()));
}
sourcePath = adjustPath(sourcePath, sourceDelegate);
FileSystem targetDelegate = getDelegate(targetPath);
if (!targetDelegate.supportsModifications(targetPath)) {
throw new UnsupportedOperationException(
String.format(
"The filesystem for the target path %s does not support modifications.",
targetPath.getPathString()));
}
targetPath = adjustPath(targetPath, targetDelegate);
if (sourceDelegate == targetDelegate) {
// Easy, same filesystem.
sourceDelegate.renameTo(sourcePath, targetPath);
return;
} else {
// Copy across filesystems, then delete.
// copyFile throws on failure, so delete will never be reached if it fails.
FileSystemUtils.copyFile(sourcePath, targetPath);
sourceDelegate.delete(sourcePath);
}
}
@Override
protected void createFSDependentHardLink(Path linkPath, Path originalPath) throws IOException {
checkModifiable(linkPath);
originalPath = internalResolveSymlink(originalPath);
FileSystem originalDelegate = getDelegate(originalPath);
FileSystem linkDelegate = getDelegate(linkPath);
if (!originalDelegate.equals(linkDelegate)
|| !linkDelegate.supportsHardLinksNatively(linkPath)) {
throw new UnsupportedOperationException(
"Attempted to create a hard link, but hard link support is disabled.");
}
linkDelegate.createFSDependentHardLink(
adjustPath(linkPath, linkDelegate), adjustPath(originalPath, originalDelegate));
}
private Path internalResolveSymlink(Path path) throws IOException {
while (isSymbolicLink(path)) {
PathFragment pathFragment = resolveOneLink(path);
path = path.getRelative(pathFragment);
}
return path;
}
}