diff options
Diffstat (limited to 'jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java')
-rw-r--r-- | jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java | 737 |
1 files changed, 737 insertions, 0 deletions
diff --git a/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java b/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java new file mode 100644 index 0000000..62e8739 --- /dev/null +++ b/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java @@ -0,0 +1,737 @@ +/* + * Copyright 2013 Google Inc. + * + * 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.common.jimfs; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.CREATE_NEW; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.nio.file.StandardOpenOption.WRITE; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Lists; +import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileSystemException; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.SecureDirectoryStream; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +/** + * View of a file system with a specific working directory. As all file system operations need to + * work when given either relative or absolute paths, this class contains the implementation of most + * file system operations, with relative path operations resolving against the working directory. + * + * <p>A file system has one default view using the file system's working directory. Additional views + * may be created for use in {@link SecureDirectoryStream} instances, which each have a different + * working directory they use. + * + * @author Colin Decker + */ +final class FileSystemView { + + private final JimfsFileStore store; + + private final Directory workingDirectory; + private final JimfsPath workingDirectoryPath; + + /** Creates a new file system view. */ + public FileSystemView( + JimfsFileStore store, Directory workingDirectory, JimfsPath workingDirectoryPath) { + this.store = checkNotNull(store); + this.workingDirectory = checkNotNull(workingDirectory); + this.workingDirectoryPath = checkNotNull(workingDirectoryPath); + } + + /** Returns whether or not this view and the given view belong to the same file system. */ + private boolean isSameFileSystem(FileSystemView other) { + return store == other.store; + } + + /** Returns the file system state. */ + public FileSystemState state() { + return store.state(); + } + + /** + * Returns the path of the working directory at the time this view was created. Does not reflect + * changes to the path caused by the directory being moved. + */ + public JimfsPath getWorkingDirectoryPath() { + return workingDirectoryPath; + } + + /** Attempt to look up the file at the given path. */ + DirectoryEntry lookUpWithLock(JimfsPath path, Set<? super LinkOption> options) + throws IOException { + store.readLock().lock(); + try { + return lookUp(path, options); + } finally { + store.readLock().unlock(); + } + } + + /** Looks up the file at the given path without locking. */ + private DirectoryEntry lookUp(JimfsPath path, Set<? super LinkOption> options) + throws IOException { + return store.lookUp(workingDirectory, path, options); + } + + /** + * Creates a new directory stream for the directory located by the given path. The given {@code + * basePathForStream} is that base path that the returned stream will use. This will be the same + * as {@code dir} except for streams created relative to another secure stream. + */ + public DirectoryStream<Path> newDirectoryStream( + JimfsPath dir, + DirectoryStream.Filter<? super Path> filter, + Set<? super LinkOption> options, + JimfsPath basePathForStream) + throws IOException { + Directory file = (Directory) lookUpWithLock(dir, options).requireDirectory(dir).file(); + FileSystemView view = new FileSystemView(store, file, basePathForStream); + JimfsSecureDirectoryStream stream = new JimfsSecureDirectoryStream(view, filter, state()); + return store.supportsFeature(Feature.SECURE_DIRECTORY_STREAM) + ? stream + : new DowngradedDirectoryStream(stream); + } + + /** Snapshots the entries of the working directory of this view. */ + public ImmutableSortedSet<Name> snapshotWorkingDirectoryEntries() { + store.readLock().lock(); + try { + ImmutableSortedSet<Name> names = workingDirectory.snapshot(); + workingDirectory.updateAccessTime(); + return names; + } finally { + store.readLock().unlock(); + } + } + + /** + * Returns a snapshot mapping the names of each file in the directory at the given path to the + * last modified time of that file. + */ + public ImmutableMap<Name, Long> snapshotModifiedTimes(JimfsPath path) throws IOException { + ImmutableMap.Builder<Name, Long> modifiedTimes = ImmutableMap.builder(); + + store.readLock().lock(); + try { + Directory dir = (Directory) lookUp(path, Options.FOLLOW_LINKS).requireDirectory(path).file(); + // TODO(cgdecker): Investigate whether WatchServices should keep a reference to the actual + // directory when SecureDirectoryStream is supported rather than looking up the directory + // each time the WatchService polls + + for (DirectoryEntry entry : dir) { + if (!entry.name().equals(Name.SELF) && !entry.name().equals(Name.PARENT)) { + modifiedTimes.put(entry.name(), entry.file().getLastModifiedTime()); + } + } + + return modifiedTimes.build(); + } finally { + store.readLock().unlock(); + } + } + + /** + * Returns whether or not the two given paths locate the same file. The second path is located + * using the given view rather than this file view. + */ + public boolean isSameFile(JimfsPath path, FileSystemView view2, JimfsPath path2) + throws IOException { + if (!isSameFileSystem(view2)) { + return false; + } + + store.readLock().lock(); + try { + File file = lookUp(path, Options.FOLLOW_LINKS).fileOrNull(); + File file2 = view2.lookUp(path2, Options.FOLLOW_LINKS).fileOrNull(); + return file != null && Objects.equals(file, file2); + } finally { + store.readLock().unlock(); + } + } + + /** + * Gets the {@linkplain Path#toRealPath(LinkOption...) real path} to the file located by the given + * path. + */ + public JimfsPath toRealPath( + JimfsPath path, PathService pathService, Set<? super LinkOption> options) throws IOException { + checkNotNull(path); + checkNotNull(options); + + store.readLock().lock(); + try { + DirectoryEntry entry = lookUp(path, options).requireExists(path); + + List<Name> names = new ArrayList<>(); + names.add(entry.name()); + while (!entry.file().isRootDirectory()) { + entry = entry.directory().entryInParent(); + names.add(entry.name()); + } + + // names are ordered last to first in the list, so get the reverse view + List<Name> reversed = Lists.reverse(names); + Name root = reversed.remove(0); + return pathService.createPath(root, reversed); + } finally { + store.readLock().unlock(); + } + } + + /** + * Creates a new directory at the given path. The given attributes will be set on the new file if + * possible. + */ + public Directory createDirectory(JimfsPath path, FileAttribute<?>... attrs) throws IOException { + return (Directory) createFile(path, store.directoryCreator(), true, attrs); + } + + /** + * Creates a new symbolic link at the given path with the given target. The given attributes will + * be set on the new file if possible. + */ + public SymbolicLink createSymbolicLink( + JimfsPath path, JimfsPath target, FileAttribute<?>... attrs) throws IOException { + if (!store.supportsFeature(Feature.SYMBOLIC_LINKS)) { + throw new UnsupportedOperationException(); + } + return (SymbolicLink) createFile(path, store.symbolicLinkCreator(target), true, attrs); + } + + /** + * Creates a new file at the given path if possible, using the given supplier to create the file. + * Returns the new file. If {@code allowExisting} is {@code true} and a file already exists at the + * given path, returns that file. Otherwise, throws {@link FileAlreadyExistsException}. + */ + private File createFile( + JimfsPath path, + Supplier<? extends File> fileCreator, + boolean failIfExists, + FileAttribute<?>... attrs) + throws IOException { + checkNotNull(path); + checkNotNull(fileCreator); + + store.writeLock().lock(); + try { + DirectoryEntry entry = lookUp(path, Options.NOFOLLOW_LINKS); + + if (entry.exists()) { + if (failIfExists) { + throw new FileAlreadyExistsException(path.toString()); + } + + // currently can only happen if getOrCreateFile doesn't find the file with the read lock + // and then the file is created between when it releases the read lock and when it + // acquires the write lock; so, very unlikely + return entry.file(); + } + + Directory parent = entry.directory(); + + File newFile = fileCreator.get(); + store.setInitialAttributes(newFile, attrs); + parent.link(path.name(), newFile); + parent.updateModifiedTime(); + return newFile; + } finally { + store.writeLock().unlock(); + } + } + + /** + * Gets the regular file at the given path, creating it if it doesn't exist and the given options + * specify that it should be created. + */ + public RegularFile getOrCreateRegularFile( + JimfsPath path, Set<OpenOption> options, FileAttribute<?>... attrs) throws IOException { + checkNotNull(path); + + if (!options.contains(CREATE_NEW)) { + // assume file exists unless we're explicitly trying to create a new file + RegularFile file = lookUpRegularFile(path, options); + if (file != null) { + return file; + } + } + + if (options.contains(CREATE) || options.contains(CREATE_NEW)) { + return getOrCreateRegularFileWithWriteLock(path, options, attrs); + } else { + throw new NoSuchFileException(path.toString()); + } + } + + /** + * Looks up the regular file at the given path, throwing an exception if the file isn't a regular + * file. Returns null if the file did not exist. + */ + @NullableDecl + private RegularFile lookUpRegularFile(JimfsPath path, Set<OpenOption> options) + throws IOException { + store.readLock().lock(); + try { + DirectoryEntry entry = lookUp(path, options); + if (entry.exists()) { + File file = entry.file(); + if (!file.isRegularFile()) { + throw new FileSystemException(path.toString(), null, "not a regular file"); + } + return open((RegularFile) file, options); + } else { + return null; + } + } finally { + store.readLock().unlock(); + } + } + + /** Gets or creates a new regular file with a write lock (assuming the file does not exist). */ + private RegularFile getOrCreateRegularFileWithWriteLock( + JimfsPath path, Set<OpenOption> options, FileAttribute<?>[] attrs) throws IOException { + store.writeLock().lock(); + try { + File file = createFile(path, store.regularFileCreator(), options.contains(CREATE_NEW), attrs); + // the file already existed but was not a regular file + if (!file.isRegularFile()) { + throw new FileSystemException(path.toString(), null, "not a regular file"); + } + return open((RegularFile) file, options); + } finally { + store.writeLock().unlock(); + } + } + + /** + * Opens the given regular file with the given options, truncating it if necessary and + * incrementing its open count. Returns the given file. + */ + private static RegularFile open(RegularFile file, Set<OpenOption> options) { + if (options.contains(TRUNCATE_EXISTING) && options.contains(WRITE)) { + file.writeLock().lock(); + try { + file.truncate(0); + } finally { + file.writeLock().unlock(); + } + } + + // must be opened while holding a file store lock to ensure no race between opening and + // deleting the file + file.opened(); + + return file; + } + + /** Returns the target of the symbolic link at the given path. */ + public JimfsPath readSymbolicLink(JimfsPath path) throws IOException { + if (!store.supportsFeature(Feature.SYMBOLIC_LINKS)) { + throw new UnsupportedOperationException(); + } + + SymbolicLink symbolicLink = + (SymbolicLink) + lookUpWithLock(path, Options.NOFOLLOW_LINKS).requireSymbolicLink(path).file(); + + return symbolicLink.target(); + } + + /** + * Checks access to the file at the given path for the given modes. Since access controls are not + * implemented for this file system, this just checks that the file exists. + */ + public void checkAccess(JimfsPath path) throws IOException { + // just check that the file exists + lookUpWithLock(path, Options.FOLLOW_LINKS).requireExists(path); + } + + /** + * Creates a hard link at the given link path to the regular file at the given path. The existing + * file must exist and must be a regular file. The given file system view must belong to the same + * file system as this view. + */ + public void link(JimfsPath link, FileSystemView existingView, JimfsPath existing) + throws IOException { + checkNotNull(link); + checkNotNull(existingView); + checkNotNull(existing); + + if (!store.supportsFeature(Feature.LINKS)) { + throw new UnsupportedOperationException(); + } + + if (!isSameFileSystem(existingView)) { + throw new FileSystemException( + link.toString(), + existing.toString(), + "can't link: source and target are in different file system instances"); + } + + Name linkName = link.name(); + + // existingView is in the same file system, so just one lock is needed + store.writeLock().lock(); + try { + // we do want to follow links when finding the existing file + File existingFile = + existingView.lookUp(existing, Options.FOLLOW_LINKS).requireExists(existing).file(); + if (!existingFile.isRegularFile()) { + throw new FileSystemException( + link.toString(), existing.toString(), "can't link: not a regular file"); + } + + Directory linkParent = + lookUp(link, Options.NOFOLLOW_LINKS).requireDoesNotExist(link).directory(); + + linkParent.link(linkName, existingFile); + linkParent.updateModifiedTime(); + } finally { + store.writeLock().unlock(); + } + } + + /** Deletes the file at the given absolute path. */ + public void deleteFile(JimfsPath path, DeleteMode deleteMode) throws IOException { + store.writeLock().lock(); + try { + DirectoryEntry entry = lookUp(path, Options.NOFOLLOW_LINKS).requireExists(path); + delete(entry, deleteMode, path); + } finally { + store.writeLock().unlock(); + } + } + + /** Deletes the given directory entry from its parent directory. */ + private void delete(DirectoryEntry entry, DeleteMode deleteMode, JimfsPath pathForException) + throws IOException { + Directory parent = entry.directory(); + File file = entry.file(); + + checkDeletable(file, deleteMode, pathForException); + parent.unlink(entry.name()); + parent.updateModifiedTime(); + + file.deleted(); + } + + /** Mode for deleting. Determines what types of files can be deleted. */ + public enum DeleteMode { + /** Delete any file. */ + ANY, + /** Only delete non-directory files. */ + NON_DIRECTORY_ONLY, + /** Only delete directory files. */ + DIRECTORY_ONLY + } + + /** Checks that the given file can be deleted, throwing an exception if it can't. */ + private void checkDeletable(File file, DeleteMode mode, Path path) throws IOException { + if (file.isRootDirectory()) { + throw new FileSystemException(path.toString(), null, "can't delete root directory"); + } + + if (file.isDirectory()) { + if (mode == DeleteMode.NON_DIRECTORY_ONLY) { + throw new FileSystemException(path.toString(), null, "can't delete: is a directory"); + } + + checkEmpty(((Directory) file), path); + } else if (mode == DeleteMode.DIRECTORY_ONLY) { + throw new FileSystemException(path.toString(), null, "can't delete: is not a directory"); + } + + if (file == workingDirectory && !path.isAbsolute()) { + // this is weird, but on Unix at least, the file system seems to be happy to delete the + // working directory if you give the absolute path to it but fail if you use a relative path + // that resolves to the working directory (e.g. "" or ".") + throw new FileSystemException(path.toString(), null, "invalid argument"); + } + } + + /** Checks that given directory is empty, throwing {@link DirectoryNotEmptyException} if not. */ + private void checkEmpty(Directory dir, Path pathForException) throws FileSystemException { + if (!dir.isEmpty()) { + throw new DirectoryNotEmptyException(pathForException.toString()); + } + } + + /** Copies or moves the file at the given source path to the given dest path. */ + public void copy( + JimfsPath source, + FileSystemView destView, + JimfsPath dest, + Set<CopyOption> options, + boolean move) + throws IOException { + checkNotNull(source); + checkNotNull(destView); + checkNotNull(dest); + checkNotNull(options); + + boolean sameFileSystem = isSameFileSystem(destView); + + File sourceFile; + File copyFile = null; // non-null after block completes iff source file was copied + lockBoth(store.writeLock(), destView.store.writeLock()); + try { + DirectoryEntry sourceEntry = lookUp(source, options).requireExists(source); + DirectoryEntry destEntry = destView.lookUp(dest, Options.NOFOLLOW_LINKS); + + Directory sourceParent = sourceEntry.directory(); + sourceFile = sourceEntry.file(); + + Directory destParent = destEntry.directory(); + + if (move && sourceFile.isDirectory()) { + if (sameFileSystem) { + checkMovable(sourceFile, source); + checkNotAncestor(sourceFile, destParent, destView); + } else { + // move to another file system is accomplished by copy-then-delete, so the source file + // must be deletable to be moved + checkDeletable(sourceFile, DeleteMode.ANY, source); + } + } + + if (destEntry.exists()) { + if (destEntry.file().equals(sourceFile)) { + return; + } else if (options.contains(REPLACE_EXISTING)) { + destView.delete(destEntry, DeleteMode.ANY, dest); + } else { + throw new FileAlreadyExistsException(dest.toString()); + } + } + + if (move && sameFileSystem) { + // Real move on the same file system. + sourceParent.unlink(source.name()); + sourceParent.updateModifiedTime(); + + destParent.link(dest.name(), sourceFile); + destParent.updateModifiedTime(); + } else { + // Doing a copy OR a move to a different file system, which must be implemented by copy and + // delete. + + // By default, don't copy attributes. + AttributeCopyOption attributeCopyOption = AttributeCopyOption.NONE; + if (move) { + // Copy only the basic attributes of the file to the other file system, as it may not + // support all the attribute views that this file system does. This also matches the + // behavior of moving a file to a foreign file system with a different + // FileSystemProvider. + attributeCopyOption = AttributeCopyOption.BASIC; + } else if (options.contains(COPY_ATTRIBUTES)) { + // As with move, if we're copying the file to a different file system, only copy its + // basic attributes. + attributeCopyOption = + sameFileSystem ? AttributeCopyOption.ALL : AttributeCopyOption.BASIC; + } + + // Copy the file, but don't copy its content while we're holding the file store locks. + copyFile = destView.store.copyWithoutContent(sourceFile, attributeCopyOption); + destParent.link(dest.name(), copyFile); + destParent.updateModifiedTime(); + + // In order for the copy to be atomic (not strictly necessary, but seems preferable since + // we can) lock both source and copy files before leaving the file store locks. This + // ensures that users cannot observe the copy's content until the content has been copied. + // This also marks the source file as opened, preventing its content from being deleted + // until after it's copied if the source file itself is deleted in the next step. + lockSourceAndCopy(sourceFile, copyFile); + + if (move) { + // It should not be possible for delete to throw an exception here, because we already + // checked that the file was deletable above. + delete(sourceEntry, DeleteMode.ANY, source); + } + } + } finally { + destView.store.writeLock().unlock(); + store.writeLock().unlock(); + } + + if (copyFile != null) { + // Copy the content. This is done outside the above block to minimize the time spent holding + // file store locks, since copying the content of a regular file could take a (relatively) + // long time. If done inside the above block, copying using Files.copy can be slower than + // copying with an InputStream and an OutputStream if many files are being copied on + // different threads. + try { + sourceFile.copyContentTo(copyFile); + } finally { + // Unlock the files, allowing the content of the copy to be observed by the user. This also + // closes the source file, allowing its content to be deleted if it was deleted. + unlockSourceAndCopy(sourceFile, copyFile); + } + } + } + + private void checkMovable(File file, JimfsPath path) throws FileSystemException { + if (file.isRootDirectory()) { + throw new FileSystemException(path.toString(), null, "can't move root directory"); + } + } + + /** + * Acquires both write locks in a way that attempts to avoid the possibility of deadlock. Note + * that typically (when only one file system instance is involved), both locks will be the same + * lock and there will be no issue at all. + */ + private static void lockBoth(Lock sourceWriteLock, Lock destWriteLock) { + while (true) { + sourceWriteLock.lock(); + if (destWriteLock.tryLock()) { + return; + } else { + sourceWriteLock.unlock(); + } + + destWriteLock.lock(); + if (sourceWriteLock.tryLock()) { + return; + } else { + destWriteLock.unlock(); + } + } + } + + /** Checks that source is not an ancestor of dest, throwing an exception if it is. */ + private void checkNotAncestor(File source, Directory destParent, FileSystemView destView) + throws IOException { + // if dest is not in the same file system, it couldn't be in source's subdirectories + if (!isSameFileSystem(destView)) { + return; + } + + Directory current = destParent; + while (true) { + if (current.equals(source)) { + throw new IOException( + "invalid argument: can't move directory into a subdirectory of itself"); + } + + if (current.isRootDirectory()) { + return; + } else { + current = current.parent(); + } + } + } + + /** + * Locks source and copy files before copying content. Also marks the source file as opened so + * that its content won't be deleted until after the copy if it is deleted. + */ + private void lockSourceAndCopy(File sourceFile, File copyFile) { + sourceFile.opened(); + ReadWriteLock sourceLock = sourceFile.contentLock(); + if (sourceLock != null) { + sourceLock.readLock().lock(); + } + ReadWriteLock copyLock = copyFile.contentLock(); + if (copyLock != null) { + copyLock.writeLock().lock(); + } + } + + /** + * Unlocks source and copy files after copying content. Also closes the source file so its content + * can be deleted if it was deleted. + */ + private void unlockSourceAndCopy(File sourceFile, File copyFile) { + ReadWriteLock sourceLock = sourceFile.contentLock(); + if (sourceLock != null) { + sourceLock.readLock().unlock(); + } + ReadWriteLock copyLock = copyFile.contentLock(); + if (copyLock != null) { + copyLock.writeLock().unlock(); + } + sourceFile.closed(); + } + + /** Returns a file attribute view using the given lookup callback. */ + @NullableDecl + public <V extends FileAttributeView> V getFileAttributeView(FileLookup lookup, Class<V> type) { + return store.getFileAttributeView(lookup, type); + } + + /** Returns a file attribute view for the given path in this view. */ + @NullableDecl + public <V extends FileAttributeView> V getFileAttributeView( + final JimfsPath path, Class<V> type, final Set<? super LinkOption> options) { + return store.getFileAttributeView( + new FileLookup() { + @Override + public File lookup() throws IOException { + return lookUpWithLock(path, options).requireExists(path).file(); + } + }, + type); + } + + /** Reads attributes of the file located by the given path in this view as an object. */ + public <A extends BasicFileAttributes> A readAttributes( + JimfsPath path, Class<A> type, Set<? super LinkOption> options) throws IOException { + File file = lookUpWithLock(path, options).requireExists(path).file(); + return store.readAttributes(file, type); + } + + /** Reads attributes of the file located by the given path in this view as a map. */ + public ImmutableMap<String, Object> readAttributes( + JimfsPath path, String attributes, Set<? super LinkOption> options) throws IOException { + File file = lookUpWithLock(path, options).requireExists(path).file(); + return store.readAttributes(file, attributes); + } + + /** + * Sets the given attribute to the given value on the file located by the given path in this view. + */ + public void setAttribute( + JimfsPath path, String attribute, Object value, Set<? super LinkOption> options) + throws IOException { + File file = lookUpWithLock(path, options).requireExists(path).file(); + store.setAttribute(file, attribute, value); + } +} |