blob: d2d9c000966785681d04e1ea365c66d2bfa859ec [file] [log] [blame]
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01001// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.vfs;
16
17import com.google.common.base.Preconditions;
18import com.google.common.collect.Lists;
19import com.google.devtools.build.lib.concurrent.ThreadSafety;
20import com.google.devtools.build.lib.util.StringTrie;
21
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.OutputStream;
25import java.util.Collection;
26import java.util.Map;
27
28import javax.annotation.Nullable;
29
30/**
31 * Presents a unified view of multiple virtual {@link FileSystem} instances, to which requests are
32 * delegated based on a {@link PathFragment} prefix mapping.
33 * If multiple prefixes apply to a given path, the *longest* (i.e. most specific) match is used.
34 * The order in which the delegates are specified does not influence the mapping.
35 *
36 * <p>Paths are preserved absolutely, contrary to how "mount" works, e.g.:
37 * /foo/bar maps to /foo/bar on the delegate, even if it is mounted at /foo.
38 *
39 * <p>For example:
40 * "/in" maps to InFileSystem, "/" maps to OtherFileSystem.
41 * Reading from "/in/base/BUILD" through the UnionFileSystem will delegate the read operation to
42 * InFileSystem, which will read "/in/base/BUILD" relative to its root.
43 * ("mount" behavior would remap it to "/base/BUILD" on the delegate).
44 *
45 * <p>Intra-filesystem symbolic links are resolved to their ultimate targets.
46 * Cross-filesystem links are not currently supported.
47 */
48@ThreadSafety.ThreadSafe
49public class UnionFileSystem extends FileSystem {
50
51 // Prefix trie index, allowing children to easily inherit prefix mappings
52 // of their parents.
53 // This does not currently handle unicode filenames.
54 private StringTrie<FileSystem> pathDelegate;
55
56 // True iff the filesystem can be modified. If false, mutating operations
57 // will throw UnsupportedOperationExceptions.
58 private final boolean readOnly;
59
60 /**
61 * Creates a new modifiable UnionFileSystem with prefix mappings
62 * specified by a map.
63 *
64 * @param prefixMapping map of path prefixes to {@link FileSystem}s
65 */
66 public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping,
67 FileSystem rootFileSystem) {
68 this(prefixMapping, rootFileSystem, /* readOnly */ false);
69 }
70
71 /**
72 * Creates a new modifiable or read-only UnionFileSystem with prefix mappings
73 * specified by a map.
74 *
75 * @param prefixMapping map of path prefixes to delegate {@link FileSystem}s
76 * @param rootFileSystem root for default requests; i.e. mapping of "/"
77 * @param readOnly if true, mutating operations will throw
78 */
79 public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping,
80 FileSystem rootFileSystem, boolean readOnly) {
81 super();
82 Preconditions.checkNotNull(prefixMapping);
83 Preconditions.checkNotNull(rootFileSystem);
84 Preconditions.checkArgument(rootFileSystem != this, "Circular root filesystem.");
85 Preconditions.checkArgument(
86 !prefixMapping.containsKey(PathFragment.EMPTY_FRAGMENT),
87 "Attempted to specify an explicit root prefix mapping; " +
88 "please use the rootFileSystem argument instead.");
89
90 this.readOnly = readOnly;
Ulf Adams07dba942015-03-05 14:47:37 +000091 this.pathDelegate = new StringTrie<>();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010092
93 for (Map.Entry<PathFragment, FileSystem> prefix : prefixMapping.entrySet()) {
94 FileSystem delegate = prefix.getValue();
95 PathFragment prefixPath = prefix.getKey();
96
97 // Extra slash prevents within-directory mappings, which Path can't handle.
98 String path = prefixPath.getPathString();
99 pathDelegate.put(path, delegate);
100 }
101 pathDelegate.put(PathFragment.EMPTY_FRAGMENT.getPathString(), rootFileSystem);
102 }
103
104 /**
105 * Retrieves the filesystem delegate of a path mapping.
106 * Does not follow symlinks (but you can call on a path preprocessed with
107 * {@link #resolveSymbolicLinks} to support this use case).
108 *
109 * @param path the {@link Path} to map to a filesystem
110 * @throws IllegalArgumentException if no delegate exists for the path
111 */
112 protected FileSystem getDelegate(Path path) {
113 Preconditions.checkNotNull(path);
114
115 String pathString = path.getPathString();
116 FileSystem immediateDelegate = pathDelegate.get(pathString);
117
118 // Should never actually happen if the root delegate is present.
119 Preconditions.checkArgument(immediateDelegate != null, "No delegate filesystem exists for %s",
120 pathString);
121 return immediateDelegate;
122 }
123
124 // Associates the path with the root of the given delegate filesystem.
125 // Necessary to avoid null pointer problems inside of the delegates.
126 protected Path adjustPath(Path path, FileSystem delegate) {
127 return delegate.getPath(path.asFragment());
128 }
129
130 /**
131 * Follow a symbolic link once using the appropriate delegate filesystem, also
132 * resolving parent directory symlinks.
133 *
134 * @param path {@link Path} to the symbolic link
135 */
136 @Override
137 protected PathFragment readSymbolicLink(Path path) throws IOException {
138 Preconditions.checkNotNull(path);
139 FileSystem delegate = getDelegate(path);
140 return delegate.readSymbolicLink(adjustPath(path, delegate));
141 }
142
143 @Override
144 protected PathFragment resolveOneLink(Path path) throws IOException {
145 Preconditions.checkNotNull(path);
146 FileSystem delegate = getDelegate(path);
147 return delegate.resolveOneLink(adjustPath(path, delegate));
148 }
149
150 private void checkModifiable() {
151 if (!supportsModifications()) {
152 throw new UnsupportedOperationException(
153 "Modifications to this " + getClass().getSimpleName() + " are disabled.");
154 }
155 }
156
157 @Override
158 public boolean supportsModifications() {
159 return !readOnly;
160 }
161
162 @Override
163 public boolean supportsSymbolicLinks() {
164 return true;
165 }
166
167 @Override
168 public String getFileSystemType(Path path) {
169 FileSystem delegate = getDelegate(path);
170 return delegate.getFileSystemType(path);
171 }
172
173 @Override
174 protected byte[] getMD5Digest(Path path) throws IOException {
175 FileSystem delegate = getDelegate(path);
176 return delegate.getMD5Digest(adjustPath(path, delegate));
177 }
178
179 @Override
180 protected boolean createDirectory(Path path) throws IOException {
181 checkModifiable();
182 // When creating the exact directory that is mapped,
183 // create it on both the parent's delegate and the path's delegate.
184 // This is necessary both for the parent to see the directory and for the
185 // delegate to use it.
186 // This is present to address this problematic case:
187 // / -> RootFs
188 // /foo -> FooFs
189 // mkdir /foo
190 // ls / ("foo" would be missing if not created on the parent)
191 // ls /foo (would fail if foo weren't also present on the child)
192 FileSystem delegate = getDelegate(path);
193 Path parent = path.getParentDirectory();
194 if (parent != null) {
195 FileSystem parentDelegate = getDelegate(parent);
196 if (parentDelegate != delegate) {
197 // There's a possibility it already exists on the parent, so don't die
198 // if the directory can't be created there.
199 parentDelegate.createDirectory(adjustPath(path, parentDelegate));
200 }
201 }
202 return delegate.createDirectory(adjustPath(path, delegate));
203 }
204
205 @Override
206 protected long getFileSize(Path path, boolean followSymlinks) throws IOException {
207 FileSystem delegate = getDelegate(path);
208 return delegate.getFileSize(adjustPath(path, delegate), followSymlinks);
209 }
210
211 @Override
212 protected boolean delete(Path path) throws IOException {
213 checkModifiable();
214 FileSystem delegate = getDelegate(path);
215 return delegate.delete(adjustPath(path, delegate));
216 }
217
218 @Override
219 protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException {
220 FileSystem delegate = getDelegate(path);
221 return delegate.getLastModifiedTime(adjustPath(path, delegate), followSymlinks);
222 }
223
224 @Override
225 protected void setLastModifiedTime(Path path, long newTime) throws IOException {
226 checkModifiable();
227 FileSystem delegate = getDelegate(path);
228 delegate.setLastModifiedTime(adjustPath(path, delegate), newTime);
229 }
230
231 @Override
232 protected boolean isSymbolicLink(Path path) {
233 FileSystem delegate = getDelegate(path);
234 path = adjustPath(path, delegate);
235 return delegate.isSymbolicLink(path);
236 }
237
238 @Override
239 protected boolean isDirectory(Path path, boolean followSymlinks) {
240 FileSystem delegate = getDelegate(path);
241 return delegate.isDirectory(adjustPath(path, delegate), followSymlinks);
242 }
243
244 @Override
245 protected boolean isFile(Path path, boolean followSymlinks) {
246 FileSystem delegate = getDelegate(path);
247 return delegate.isFile(adjustPath(path, delegate), followSymlinks);
248 }
249
250 @Override
251 protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException {
252 checkModifiable();
253 if (!supportsSymbolicLinks()) {
254 throw new UnsupportedOperationException(
255 "Attempted to create a symlink, but symlink support is disabled.");
256 }
257
258 FileSystem delegate = getDelegate(linkPath);
259 delegate.createSymbolicLink(adjustPath(linkPath, delegate), targetFragment);
260 }
261
262 @Override
263 protected boolean exists(Path path, boolean followSymlinks) {
264 FileSystem delegate = getDelegate(path);
265 return delegate.exists(adjustPath(path, delegate), followSymlinks);
266 }
267
268 @Override
269 protected FileStatus stat(final Path path, final boolean followSymlinks) throws IOException {
270 FileSystem delegate = getDelegate(path);
271 return delegate.stat(adjustPath(path, delegate), followSymlinks);
272 }
273
274 // Needs to be overridden for the delegation logic, because the
275 // UnixFileSystem implements statNullable and stat as separate codepaths.
276 // More generally, we wish to delegate all filesystem operations.
277 @Override
278 protected FileStatus statNullable(Path path, boolean followSymlinks) {
279 FileSystem delegate = getDelegate(path);
280 return delegate.statNullable(adjustPath(path, delegate), followSymlinks);
281 }
282
283 @Override
284 @Nullable
285 protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
286 FileSystem delegate = getDelegate(path);
287 return delegate.statIfFound(adjustPath(path, delegate), followSymlinks);
288 }
289
290 /**
291 * Retrieves the directory entries for the specified path under the assumption
292 * that {@code resolvedPath} is the resolved path of {@code path} in one of the
293 * underlying file systems.
294 *
295 * @param path the {@link Path} whose children are to be retrieved
296 */
297 @Override
298 protected Collection<Path> getDirectoryEntries(Path path) throws IOException {
299 FileSystem delegate = getDelegate(path);
300 Path resolvedPath = adjustPath(path, delegate);
301 Collection<Path> entries = resolvedPath.getDirectoryEntries();
302 Collection<Path> result = Lists.newArrayListWithCapacity(entries.size());
303 for (Path entry : entries) {
304 result.add(path.getChild(entry.getBaseName()));
305 }
306 return result;
307 }
308
309 // No need for the more complex logic of getDirectoryEntries; it calls it implicitly.
310 @Override
311 protected Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
312 FileSystem delegate = getDelegate(path);
313 return delegate.readdir(adjustPath(path, delegate), followSymlinks);
314 }
315
316 @Override
317 protected boolean isReadable(Path path) throws IOException {
318 FileSystem delegate = getDelegate(path);
319 return delegate.isReadable(adjustPath(path, delegate));
320 }
321
322 @Override
323 protected void setReadable(Path path, boolean readable) throws IOException {
324 checkModifiable();
325 FileSystem delegate = getDelegate(path);
326 delegate.setReadable(adjustPath(path, delegate), readable);
327 }
328
329 @Override
330 protected boolean isWritable(Path path) throws IOException {
331 if (!supportsModifications()) {
332 return false;
333 }
334 FileSystem delegate = getDelegate(path);
335 return delegate.isWritable(adjustPath(path, delegate));
336 }
337
338 @Override
339 protected void setWritable(Path path, boolean writable) throws IOException {
340 checkModifiable();
341 FileSystem delegate = getDelegate(path);
342 delegate.setWritable(adjustPath(path, delegate), writable);
343 }
344
345 @Override
346 protected boolean isExecutable(Path path) throws IOException {
347 FileSystem delegate = getDelegate(path);
348 return delegate.isExecutable(adjustPath(path, delegate));
349 }
350
351 @Override
352 protected void setExecutable(Path path, boolean executable) throws IOException {
353 checkModifiable();
354 FileSystem delegate = getDelegate(path);
355 delegate.setExecutable(adjustPath(path, delegate), executable);
356 }
357
358 @Override
359 protected String getFastDigestFunctionType(Path path) {
360 FileSystem delegate = getDelegate(path);
361 return delegate.getFastDigestFunctionType(adjustPath(path, delegate));
362 }
363
364 @Override
365 protected byte[] getFastDigest(Path path) throws IOException {
366 FileSystem delegate = getDelegate(path);
367 return delegate.getFastDigest(adjustPath(path, delegate));
368 }
369
370 @Override
371 protected byte[] getxattr(Path path, String name, boolean followSymlinks) throws IOException {
372 FileSystem delegate = getDelegate(path);
373 return delegate.getxattr(adjustPath(path, delegate), name, followSymlinks);
374 }
375
376 @Override
377 protected InputStream getInputStream(Path path) throws IOException {
378 FileSystem delegate = getDelegate(path);
379 return delegate.getInputStream(adjustPath(path, delegate));
380 }
381
382 @Override
383 protected OutputStream getOutputStream(Path path, boolean append) throws IOException {
384 checkModifiable();
385 FileSystem delegate = getDelegate(path);
386 return delegate.getOutputStream(adjustPath(path, delegate), append);
387 }
388
389 @Override
390 protected void renameTo(Path sourcePath, Path targetPath) throws IOException {
391 checkModifiable();
392 FileSystem sourceDelegate = getDelegate(sourcePath);
393 if (!sourceDelegate.supportsModifications()) {
394 throw new UnsupportedOperationException(
395 "The filesystem for the source path "
396 + sourcePath.getPathString() + " does not support modifications.");
397 }
398 sourcePath = adjustPath(sourcePath, sourceDelegate);
399
400 FileSystem targetDelegate = getDelegate(targetPath);
401 if (!targetDelegate.supportsModifications()) {
402 throw new UnsupportedOperationException(
403 "The filesystem for the target path "
404 + targetPath.getPathString() + " does not support modifications.");
405 }
406 targetPath = adjustPath(targetPath, targetDelegate);
407
408 if (sourceDelegate == targetDelegate) {
409 // Easy, same filesystem.
410 sourceDelegate.renameTo(sourcePath, targetPath);
411 return;
412 } else {
413 // Copy across filesystems, then delete.
414 // copyFile throws on failure, so delete will never be reached if it fails.
415 FileSystemUtils.copyFile(sourcePath, targetPath);
416 sourceDelegate.delete(sourcePath);
417 }
418 }
419}