// 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.collect.Lists; import com.google.devtools.build.lib.concurrent.ThreadSafety; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.StringTrie; import com.google.devtools.build.lib.vfs.FileSystem.HashFunction; 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 StringTrie<FileSystem> pathDelegate; // True iff the filesystem can be modified. If false, mutating operations // will throw UnsupportedOperationExceptions. private final boolean readOnly; // 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 */ public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping, FileSystem rootFileSystem) { this(prefixMapping, rootFileSystem, /* readOnly */ false); } /** * Creates a new modifiable or read-only UnionFileSystem with prefix mappings * specified by a map. * * @param prefixMapping map of path prefixes to delegate {@link FileSystem}s * @param rootFileSystem root for default requests; i.e. mapping of "/" * @param readOnly if true, mutating operations will throw */ public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping, FileSystem rootFileSystem, boolean readOnly) { 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.readOnly = readOnly; this.pathDelegate = new StringTrie<>(); 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. String path = prefixPath.getPathString(); pathDelegate.put(path, delegate); } pathDelegate.put(PathFragment.EMPTY_FRAGMENT.getPathString(), 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); String pathString = path.getPathString(); FileSystem immediateDelegate = pathDelegate.get(pathString); // Should never actually happen if the root delegate is present. Preconditions.checkArgument(immediateDelegate != null, "No delegate filesystem exists for %s", pathString); 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() { if (!supportsModifications()) { throw new UnsupportedOperationException( "Modifications to this " + getClass().getSimpleName() + " are disabled."); } } @Override public boolean supportsModifications() { return !readOnly; } @Override public boolean supportsSymbolicLinksNatively() { return true; } @Override public boolean supportsHardLinksNatively() { return true; } @Override public boolean isFilePathCaseSensitive() { return this.isCaseSensitive; } @Override public String getFileSystemType(Path path) { FileSystem delegate = getDelegate(path); return delegate.getFileSystemType(path); } @Override protected byte[] getMD5Digest(Path path) throws IOException { FileSystem delegate = getDelegate(path); return delegate.getMD5Digest(adjustPath(path, delegate)); } @Override protected boolean createDirectory(Path path) throws IOException { checkModifiable(); // 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) { 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 { FileSystem delegate = getDelegate(path); return delegate.getFileSize(adjustPath(path, delegate), followSymlinks); } @Override protected boolean delete(Path path) throws IOException { checkModifiable(); FileSystem delegate = getDelegate(path); return delegate.delete(adjustPath(path, delegate)); } @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException { FileSystem delegate = getDelegate(path); return delegate.getLastModifiedTime(adjustPath(path, delegate), followSymlinks); } @Override protected void setLastModifiedTime(Path path, long newTime) throws IOException { checkModifiable(); 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) { FileSystem delegate = getDelegate(path); return delegate.isDirectory(adjustPath(path, delegate), followSymlinks); } @Override protected boolean isFile(Path path, boolean followSymlinks) { FileSystem delegate = getDelegate(path); return delegate.isFile(adjustPath(path, delegate), followSymlinks); } @Override protected boolean isSpecialFile(Path path, boolean followSymlinks) { FileSystem delegate = getDelegate(path); return delegate.isSpecialFile(adjustPath(path, delegate), followSymlinks); } @Override protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException { checkModifiable(); if (!supportsSymbolicLinksNatively()) { 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) { FileSystem delegate = getDelegate(path); return delegate.exists(adjustPath(path, delegate), followSymlinks); } @Override protected FileStatus stat(final Path path, final boolean followSymlinks) throws IOException { FileSystem delegate = getDelegate(path); return delegate.stat(adjustPath(path, delegate), followSymlinks); } // 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) { FileSystem delegate = getDelegate(path); return delegate.statNullable(adjustPath(path, delegate), followSymlinks); } @Override @Nullable protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException { FileSystem delegate = getDelegate(path); return delegate.statIfFound(adjustPath(path, delegate), followSymlinks); } /** * 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<Path> getDirectoryEntries(Path path) throws IOException { FileSystem delegate = getDelegate(path); Path resolvedPath = adjustPath(path, delegate); Collection<Path> entries = resolvedPath.getDirectoryEntries(); Collection<Path> result = Lists.newArrayListWithCapacity(entries.size()); for (Path entry : entries) { result.add(path.getChild(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 { FileSystem delegate = getDelegate(path); return delegate.readdir(adjustPath(path, delegate), followSymlinks); } @Override protected boolean isReadable(Path path) throws IOException { FileSystem delegate = getDelegate(path); return delegate.isReadable(adjustPath(path, delegate)); } @Override protected void setReadable(Path path, boolean readable) throws IOException { checkModifiable(); FileSystem delegate = getDelegate(path); delegate.setReadable(adjustPath(path, delegate), readable); } @Override protected boolean isWritable(Path path) throws IOException { if (!supportsModifications()) { return false; } FileSystem delegate = getDelegate(path); return delegate.isWritable(adjustPath(path, delegate)); } @Override protected void setWritable(Path path, boolean writable) throws IOException { checkModifiable(); FileSystem delegate = getDelegate(path); delegate.setWritable(adjustPath(path, delegate), writable); } @Override protected boolean isExecutable(Path path) throws IOException { FileSystem delegate = getDelegate(path); return delegate.isExecutable(adjustPath(path, delegate)); } @Override protected void setExecutable(Path path, boolean executable) throws IOException { checkModifiable(); FileSystem delegate = getDelegate(path); delegate.setExecutable(adjustPath(path, delegate), executable); } @Override protected byte[] getFastDigest(Path path, HashFunction hashFunction) throws IOException { FileSystem delegate = getDelegate(path); return delegate.getFastDigest(adjustPath(path, delegate), hashFunction); } @Override protected byte[] getxattr(Path path, String name) throws IOException { FileSystem delegate = getDelegate(path); return delegate.getxattr(adjustPath(path, delegate), name); } @Override protected InputStream getInputStream(Path path) throws IOException { FileSystem delegate = getDelegate(path); return delegate.getInputStream(adjustPath(path, delegate)); } @Override protected OutputStream getOutputStream(Path path, boolean append) throws IOException { checkModifiable(); FileSystem delegate = getDelegate(path); return delegate.getOutputStream(adjustPath(path, delegate), append); } @Override protected void renameTo(Path sourcePath, Path targetPath) throws IOException { checkModifiable(); FileSystem sourceDelegate = getDelegate(sourcePath); if (!sourceDelegate.supportsModifications()) { throw new UnsupportedOperationException( "The filesystem for the source path " + sourcePath.getPathString() + " does not support modifications."); } sourcePath = adjustPath(sourcePath, sourceDelegate); FileSystem targetDelegate = getDelegate(targetPath); if (!targetDelegate.supportsModifications()) { throw new UnsupportedOperationException( "The filesystem for the target path " + targetPath.getPathString() + " does not support modifications."); } 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(); FileSystem originalDelegate = getDelegate(originalPath); FileSystem linkDelegate = getDelegate(linkPath); if (!originalDelegate.equals(linkDelegate) || !linkDelegate.supportsHardLinksNatively()) { throw new UnsupportedOperationException( "Attempted to create a hard link, but hard link support is disabled."); } linkDelegate.createFSDependentHardLink( adjustPath(linkPath, linkDelegate), adjustPath(originalPath, originalDelegate)); } }