// 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. * *

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. * *

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). * *

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 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 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 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 public 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 public void createDirectoryAndParents(Path path) throws IOException { checkModifiable(path); FileSystem delegate = getDelegate(path); delegate.createDirectoryAndParents(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 public 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 public 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 getDirectoryEntries(Path path) throws IOException { path = internalResolveSymlink(path); FileSystem delegate = getDelegate(path); Path resolvedPath = adjustPath(path, delegate); Collection entries = resolvedPath.getDirectoryEntries(); Collection 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 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 public 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 public 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 public 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; } }