// 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.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.StringCanonicalizer; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.Collection; import java.util.IdentityHashMap; import java.util.Objects; /** *

Instances of this class represent pathnames, forming a tree * structure to implement sharing of common prefixes (parent directory names). * A node in these trees is something like foo, bar, .., ., or /. If the * instance is not a root path, it will have a parent path. A path can also * have children, which are indexed by name in a map. * *

There is some limited support for Windows-style paths. Most importantly, drive identifiers * in front of a path (c:/abc) are supported. However, Windows-style backslash separators * (C:\\foo\\bar) and drive-relative paths ("C:foo") are explicitly not supported, same with * advanced features like \\\\network\\paths and \\\\?\\unc\\paths. * *

{@link FileSystem} implementations maintain pointers into this graph. */ @ThreadSafe public class Path implements Comparable, Serializable { private static FileSystem fileSystemForSerialization; /** * We need to specify used FileSystem. In this case we can save memory during the serialization. */ public static void setFileSystemForSerialization(FileSystem fileSystem) { fileSystemForSerialization = fileSystem; } /** * Returns FileSystem that we are using. */ public static FileSystem getFileSystemForSerialization() { return fileSystemForSerialization; } // These are basically final, but can't be marked as such in order to support serialization. private FileSystem fileSystem; private String name; private Path parent; private int depth; private int hashCode; private static final ReferenceQueue REFERENCE_QUEUE = new ReferenceQueue<>(); private static class PathWeakReferenceForCleanup extends WeakReference { final Path parent; final String baseName; PathWeakReferenceForCleanup(Path referent, ReferenceQueue referenceQueue) { super(referent, referenceQueue); parent = referent.getParentDirectory(); baseName = referent.getBaseName(); } } private static final Thread PATH_CHILD_CACHE_CLEANUP_THREAD = new Thread("Path cache cleanup") { @Override public void run() { while (true) { try { PathWeakReferenceForCleanup ref = (PathWeakReferenceForCleanup) REFERENCE_QUEUE.remove(); Path parent = ref.parent; synchronized (parent) { // It's possible that since this reference was enqueued for deletion, the Path was // recreated with a new entry in the map. We definitely shouldn't delete that entry, so // check to make sure they're the same. Reference currentRef = ref.parent.children.get(ref.baseName); if (currentRef == ref) { ref.parent.children.remove(ref.baseName); } } } catch (InterruptedException e) { // Ignored. } } } }; static { PATH_CHILD_CACHE_CLEANUP_THREAD.setDaemon(true); PATH_CHILD_CACHE_CLEANUP_THREAD.start(); } /** * A mapping from a child file name to the {@link Path} representing it. * *

File names must be a single path segment. The strings must be * canonical. We use IdentityHashMap instead of HashMap for reasons of space * efficiency: instances are smaller by a single word. Also, since all path * segments are interned, the universe of Paths holds a minimal number of * references to strings. (It's doubtful that there's any time gain from use * of an IdentityHashMap, since the time saved by avoiding string equality * tests during hash lookups is probably equal to the time spent eagerly * interning strings, unless the collision rate is high.) * *

The Paths are stored as weak references to ensure that a live * Path for a directory does not hold a strong reference to all of its * descendants, which would prevent collection of paths we never intend to * use again. Stale references in the map must be treated as absent. * *

A Path may be recycled once there is no Path that refers to it or * to one of its descendants. This means that any data stored in the * Path instance by one of its subclasses must be recoverable: it's ok to * store data in Paths as an optimization, but there must be another * source for that data in case the Path is recycled. * *

We intentionally avoid using the existing library classes for reasons of * space efficiency: while ConcurrentHashMap would reduce our locking * overhead, and ReferenceMap would simplify the code a little, both of those * classes have much higher per-instance overheads than IdentityHashMap. * *

The Path object must be synchronized while children is being * accessed. */ private volatile IdentityHashMap> children; /** * Create a path instance. Should only be called by {@link #createChildPath}. * * @param name the name of this path; it must be canonicalized with {@link * StringCanonicalizer#intern} * @param parent this path's parent */ protected Path(FileSystem fileSystem, String name, Path parent) { this.fileSystem = fileSystem; this.name = name; this.parent = parent; this.depth = parent == null ? 0 : parent.depth + 1; if (fileSystem == null || fileSystem.isFilePathCaseSensitive()) { this.hashCode = Objects.hash(parent, name); } else { this.hashCode = Objects.hash(parent, name.toLowerCase()); } } /** * Create the root path. Should only be called by * {@link FileSystem#createRootPath()}. */ protected Path(FileSystem fileSystem) { this(fileSystem, StringCanonicalizer.intern("/"), null); } private void writeObject(ObjectOutputStream out) throws IOException { Preconditions.checkState(fileSystem == fileSystemForSerialization, fileSystem); out.writeUTF(getPathString()); } private void readObject(ObjectInputStream in) throws IOException { fileSystem = fileSystemForSerialization; String p = in.readUTF(); PathFragment pf = new PathFragment(p); PathFragment parentDir = pf.getParentDirectory(); if (parentDir == null) { this.name = "/"; this.parent = null; this.depth = 0; } else { this.name = pf.getBaseName(); this.parent = fileSystem.getPath(parentDir); this.depth = this.parent.depth + 1; } this.hashCode = Objects.hash(parent, name); } /** * Returns the filesystem instance to which this path belongs. */ public FileSystem getFileSystem() { return fileSystem; } public boolean isRootDirectory() { return parent == null; } protected Path createChildPath(String childName) { return new Path(fileSystem, childName, this); } /** * Returns the child path named name, or creates such a path (and caches it) * if it doesn't already exist. */ private Path getCachedChildPath(String childName) { // We get a canonical instance since 'children' is an IdentityHashMap. childName = StringCanonicalizer.intern(childName); // We use double-checked locking so that we only hold the lock when we might need to mutate the // 'children' variable. 'children' will never become null if it's already non-null, so we only // need to worry about the case where it's currently null and we race with another thread // executing getCachedChildPath() trying to set 'children' to a non-null value. if (children == null) { synchronized (this) { if (children == null) { // 66% of Paths have size == 1, 80% <= 2 children = new IdentityHashMap<>(1); } } } synchronized (this) { Reference childRef = children.get(childName); Path child; if (childRef == null || (child = childRef.get()) == null) { child = createChildPath(childName); children.put(childName, new PathWeakReferenceForCleanup(child, REFERENCE_QUEUE)); } return child; } } /** * Applies the specified function to each {@link Path} that is an existing direct * descendant of this one. The Predicate is evaluated only for its * side-effects. * *

This function exists to hide the "children" field, whose complex * synchronization and identity requirements are too unsafe to be exposed to * subclasses. For example, the "children" field must be synchronized for * the duration of any iteration over it; it may be null; and references * within it may be stale, and must be ignored. */ protected synchronized void applyToChildren(Predicate function) { if (children != null) { for (Reference childRef : children.values()) { Path child = childRef.get(); if (child != null) { function.apply(child); } } } } /** * Returns whether this path is recursively "under" {@code prefix} - that is, * whether {@code path} is a prefix of this path. * *

This method returns {@code true} when called with this path itself. This * method acts independently of the existence of files or folders. * * @param prefix a path which may or may not be a prefix of this path */ public boolean startsWith(Path prefix) { Path n = this; for (int i = 0, len = depth - prefix.depth; i < len; i++) { n = n.getParentDirectory(); } return prefix.equals(n); } /** * Computes a string representation of this path, and writes it to the * given string builder. Only called locally with a new instance. */ private void buildPathString(StringBuilder result) { if (isRootDirectory()) { result.append('/'); } else { if (parent.isWindowsVolumeName()) { result.append(parent.name); } else { parent.buildPathString(result); } if (!parent.isRootDirectory()) { result.append('/'); } result.append(name); } } /** * Returns true if the current path represents a Windows volume name (such as "c:" or "d:"). * *

Paths such as '\\\\vol\\foo' are not supported. */ private boolean isWindowsVolumeName() { return OS.getCurrent() == OS.WINDOWS && parent != null && parent.isRootDirectory() && name.length() == 2 && PathFragment.getWindowsDriveLetter(name) != '\0'; } /** * Returns the path as a string. */ public String getPathString() { // Profile driven optimization: // Preallocate a size determined by the depth, so that // we do not have to expand the capacity of the StringBuilder StringBuilder builder = new StringBuilder(depth * 20); buildPathString(builder); return builder.toString(); } /** * Returns the path as a string. */ @Override public String toString() { return getPathString(); } @Override public int hashCode() { return hashCode; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof Path)) { return false; } Path otherPath = (Path) other; if (hashCode != otherPath.hashCode) { return false; } if (!fileSystem.equals(otherPath.fileSystem)) { return false; } if (fileSystem.isFilePathCaseSensitive()) { return name.equals(otherPath.name) && Objects.equals(parent, otherPath.parent); } else { return name.toLowerCase().equals(otherPath.name.toLowerCase()) && Objects.equals(parent, otherPath.parent); } } /** * Returns a string of debugging information associated with this path. * The results are unspecified and MUST NOT be interpreted programmatically. */ protected String toDebugString() { return ""; } /** * Returns a path representing the parent directory of this path, * or null iff this Path represents the root of the filesystem. * *

Note: This method normalises ".." and "." path segments by string * processing, not by directory lookups. */ public Path getParentDirectory() { return parent; } /** * Returns true iff this path denotes an existing file of any kind. Follows * symbolic links. */ public boolean exists() { return fileSystem.exists(this, true); } /** * Returns true iff this path denotes an existing file of any kind. * * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a * symbolic link, the link is dereferenced until a file other than a * symbolic link is found */ public boolean exists(Symlinks followSymlinks) { return fileSystem.exists(this, followSymlinks.toBoolean()); } /** * Returns a new, immutable collection containing the names of all entities * within the directory denoted by the current path. Follows symbolic links. * * @throws FileNotFoundException If the directory is not found * @throws IOException If the path does not denote a directory */ public Collection getDirectoryEntries() throws IOException, FileNotFoundException { return fileSystem.getDirectoryEntries(this); } /** * Returns a collection of the names and types of all entries within the directory * denoted by the current path. Follows symbolic links if {@code followSymlinks} is true. * Note that the order of the returned entries is not guaranteed. * * @param followSymlinks whether to follow symlinks or not * * @throws FileNotFoundException If the directory is not found * @throws IOException If the path does not denote a directory */ public Collection readdir(Symlinks followSymlinks) throws IOException { return fileSystem.readdir(this, followSymlinks.toBoolean()); } /** * Returns a new, immutable collection containing the names of all entities * within the directory denoted by the current path, for which the given * predicate is true. * * @throws FileNotFoundException If the directory is not found * @throws IOException If the path does not denote a directory */ public Collection getDirectoryEntries(Predicate predicate) throws IOException, FileNotFoundException { return ImmutableList.copyOf(Iterables.filter(getDirectoryEntries(), predicate)); } /** * Returns the status of a file, following symbolic links. * * @throws IOException if there was an error obtaining the file status. Note, * some implementations may defer the I/O, and hence the throwing of * the exception, until the accessor methods of {@code FileStatus} are * called. */ public FileStatus stat() throws IOException { return fileSystem.stat(this, true); } /** * Like stat(), but returns null on file-nonexistence instead of throwing. */ public FileStatus statNullable() { return statNullable(Symlinks.FOLLOW); } /** * Like stat(), but returns null on file-nonexistence instead of throwing. */ public FileStatus statNullable(Symlinks symlinks) { return fileSystem.statNullable(this, symlinks.toBoolean()); } /** * Returns the status of a file, optionally following symbolic links. * * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a * symbolic link, the link is dereferenced until a file other than a * symbolic link is found * @throws IOException if there was an error obtaining the file status. Note, * some implementations may defer the I/O, and hence the throwing of * the exception, until the accessor methods of {@code FileStatus} are * called */ public FileStatus stat(Symlinks followSymlinks) throws IOException { return fileSystem.stat(this, followSymlinks.toBoolean()); } /** * Like {@link #stat}, but may return null if the file is not found (corresponding to * {@code ENOENT} and {@code ENOTDIR} in Unix's stat(2) function) instead of throwing. Follows * symbolic links. */ public FileStatus statIfFound() throws IOException { return fileSystem.statIfFound(this, true); } /** * Like {@link #stat}, but may return null if the file is not found (corresponding to * {@code ENOENT} and {@code ENOTDIR} in Unix's stat(2) function) instead of throwing. * * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a * symbolic link, the link is dereferenced until a file other than a * symbolic link is found */ public FileStatus statIfFound(Symlinks followSymlinks) throws IOException { return fileSystem.statIfFound(this, followSymlinks.toBoolean()); } /** * Returns true iff this path denotes an existing directory. Follows symbolic * links. */ public boolean isDirectory() { return fileSystem.isDirectory(this, true); } /** * Returns true iff this path denotes an existing directory. * * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a * symbolic link, the link is dereferenced until a file other than a * symbolic link is found */ public boolean isDirectory(Symlinks followSymlinks) { return fileSystem.isDirectory(this, followSymlinks.toBoolean()); } /** * Returns true iff this path denotes an existing regular or special file. * Follows symbolic links. * *

For our purposes, "file" includes special files (socket, fifo, block or * char devices) too; it excludes symbolic links and directories. */ public boolean isFile() { return fileSystem.isFile(this, true); } /** * Returns true iff this path denotes an existing regular or special file. * *

For our purposes, a "file" includes special files (socket, fifo, block * or char devices) too; it excludes symbolic links and directories. * * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a * symbolic link, the link is dereferenced until a file other than a * symbolic link is found. */ public boolean isFile(Symlinks followSymlinks) { return fileSystem.isFile(this, followSymlinks.toBoolean()); } /** * Returns true iff this path denotes an existing special file (e.g. fifo). * Follows symbolic links. */ public boolean isSpecialFile() { return fileSystem.isSpecialFile(this, true); } /** * Returns true iff this path denotes an existing special file (e.g. fifo). * * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a * symbolic link, the link is dereferenced until a path other than a * symbolic link is found. */ public boolean isSpecialFile(Symlinks followSymlinks) { return fileSystem.isSpecialFile(this, followSymlinks.toBoolean()); } /** * Returns true iff this path denotes an existing symbolic link. Does not * follow symbolic links. */ public boolean isSymbolicLink() { return fileSystem.isSymbolicLink(this); } /** * Returns the last segment of this path, or "/" for the root directory. */ public String getBaseName() { return name; } /** * Interprets the name of a path segment relative to the current path and * returns the result. * *

This is a purely syntactic operation, i.e. it does no I/O, it does not * validate the existence of any path, nor resolve symbolic links. If 'prefix' * is not canonical, then a 'name' of '..' will be interpreted incorrectly. * * @precondition segment contains no slashes. */ private Path getCanonicalPath(String segment) { if (segment.equals(".") || segment.isEmpty()) { return this; // that's a noop } else if (segment.equals("..")) { // root's parent is root, when canonicalising: return parent == null || isWindowsVolumeName() ? this : parent; } else { return getCachedChildPath(segment); } } /** * Returns the path formed by appending the single non-special segment * "baseName" to this path. * *

You should almost always use {@link #getRelative} instead, which has * the same performance characteristics if the given name is a valid base * name, and which also works for '.', '..', and strings containing '/'. * * @throws IllegalArgumentException if {@code baseName} is not a valid base * name according to {@link FileSystemUtils#checkBaseName} */ public Path getChild(String baseName) { FileSystemUtils.checkBaseName(baseName); return getCachedChildPath(baseName); } /** * Returns the path formed by appending the relative or absolute path fragment * {@code suffix} to this path. * *

If suffix is absolute, the current path will be ignored; otherwise, they * will be combined. Up-level references ("..") cause the preceding path * segment to be elided; this interpretation is only correct if the base path * is canonical. */ public Path getRelative(PathFragment suffix) { Path result = suffix.isAbsolute() ? fileSystem.getRootDirectory() : this; if (!suffix.windowsVolume().isEmpty()) { result = result.getCanonicalPath(suffix.windowsVolume()); } for (String segment : suffix.segments()) { result = result.getCanonicalPath(segment); } return result; } /** * Returns the path formed by appending the relative or absolute string * {@code path} to this path. * *

If the given path string is absolute, the current path will be ignored; * otherwise, they will be combined. Up-level references ("..") cause the * preceding path segment to be elided. * *

This is a purely syntactic operation, i.e. it does no I/O, it does not * validate the existence of any path, nor resolve symbolic links. */ public Path getRelative(String path) { // Fast path for valid base names. if ((path.length() == 0) || (path.equals("."))) { return this; } else if (path.equals("..")) { return parent == null ? this : parent; } else if (path.indexOf('/') != -1) { return getRelative(new PathFragment(path)); } else if (path.indexOf(PathFragment.EXTRA_SEPARATOR_CHAR) != -1) { return getRelative(new PathFragment(path)); } else { return getCachedChildPath(path); } } /** * Returns an absolute PathFragment representing this path. */ public PathFragment asFragment() { String[] resultSegments = new String[depth]; Path currentPath = this; for (int pos = depth - 1; pos >= 0; pos--) { resultSegments[pos] = currentPath.getBaseName(); currentPath = currentPath.getParentDirectory(); } char driveLetter = '\0'; if (resultSegments.length > 0) { driveLetter = PathFragment.getWindowsDriveLetter(resultSegments[0]); if (driveLetter != '\0') { // Strip off the first segment that contains the volume name. resultSegments = Arrays.copyOfRange(resultSegments, 1, resultSegments.length); } } return new PathFragment(driveLetter, true, resultSegments); } /** * Returns a relative path fragment to this path, relative to {@code * ancestorDirectory}. {@code ancestorDirectory} must be on the same * filesystem as this path. (Currently, both this path and "ancestorDirectory" * must be absolute, though this restriction could be loosened.) *

* x.relativeTo(z) == y implies * z.getRelative(y.getPathString()) == x. *

* For example, "/foo/bar/wiz".relativeTo("/foo") returns * "bar/wiz". * * @throws IllegalArgumentException if this path is not beneath {@code * ancestorDirectory} or if they are not part of the same filesystem */ public PathFragment relativeTo(Path ancestorPath) { checkSameFilesystem(ancestorPath); // Fast path: when otherPath is the ancestor of this path int resultSegmentCount = depth - ancestorPath.depth; if (resultSegmentCount >= 0) { String[] resultSegments = new String[resultSegmentCount]; Path currentPath = this; for (int pos = resultSegmentCount - 1; pos >= 0; pos--) { resultSegments[pos] = currentPath.getBaseName(); currentPath = currentPath.getParentDirectory(); } if (ancestorPath.equals(currentPath)) { return new PathFragment('\0', false, resultSegments); } } throw new IllegalArgumentException("Path " + this + " is not beneath " + ancestorPath); } /** * Checks that "this" and "that" are paths on the same filesystem. */ protected void checkSameFilesystem(Path that) { if (this.fileSystem != that.fileSystem) { throw new IllegalArgumentException("Files are on different filesystems: " + this + ", " + that); } } /** * Returns an output stream to the file denoted by the current path, creating * it and truncating it if necessary. The stream is opened for writing. * * @throws FileNotFoundException If the file cannot be found or created. * @throws IOException If a different error occurs. */ public OutputStream getOutputStream() throws IOException, FileNotFoundException { return getOutputStream(false); } /** * Returns an output stream to the file denoted by the current path, creating * it and truncating it if necessary. The stream is opened for writing. * * @param append whether to open the file in append mode. * @throws FileNotFoundException If the file cannot be found or created. * @throws IOException If a different error occurs. */ public OutputStream getOutputStream(boolean append) throws IOException, FileNotFoundException { return fileSystem.getOutputStream(this, append); } /** * Creates a directory with the name of the current path, not following * symbolic links. Returns normally iff the directory exists after the call: * true if the directory was created by this call, false if the directory was * already in existence. Throws an exception if the directory could not be * created for any reason. * * @throws IOException if the directory creation failed for any reason */ public boolean createDirectory() throws IOException { return fileSystem.createDirectory(this); } /** * Creates a symbolic link with the name of the current path, following * symbolic links. The referent of the created symlink is is the absolute path * "target"; it is not possible to create relative symbolic links via this * method. * * @throws IOException if the creation of the symbolic link was unsuccessful * for any reason */ public void createSymbolicLink(Path target) throws IOException { checkSameFilesystem(target); fileSystem.createSymbolicLink(this, target.asFragment()); } /** * Creates a symbolic link with the name of the current path, following * symbolic links. The referent of the created symlink is is the path fragment * "target", which may be absolute or relative. * * @throws IOException if the creation of the symbolic link was unsuccessful * for any reason */ public void createSymbolicLink(PathFragment target) throws IOException { fileSystem.createSymbolicLink(this, target); } /** * Returns the target of the current path, which must be a symbolic link. The link contents are * returned exactly, and may contain an absolute or relative path. Analogous to readlink(2). * *

Note: for {@link FileSystem}s where {@link FileSystem#supportsSymbolicLinksNatively()} * returns false, this method will throw an {@link UnsupportedOperationException} if the link * points to a non-existent file. * * @return the content (i.e. target) of the symbolic link * @throws IOException if the current path is not a symbolic link, or the contents of the link * could not be read for any reason */ public PathFragment readSymbolicLink() throws IOException { return fileSystem.readSymbolicLink(this); } /** * If the current path is a symbolic link, returns the target of this symbolic link. The * semantics are intentionally left underspecified otherwise to permit efficient implementations. * * @return the content (i.e. target) of the symbolic link * @throws IOException if the current path is not a symbolic link, or the * contents of the link could not be read for any reason */ public PathFragment readSymbolicLinkUnchecked() throws IOException { return fileSystem.readSymbolicLinkUnchecked(this); } /** * Create a hard link for the current path. * * @param link the path of the new link * @throws IOException if there was an error executing {@link FileSystem#createHardLink} */ public void createHardLink(Path link) throws IOException { fileSystem.createHardLink(link, this); } /** * Returns the canonical path for this path, by repeatedly replacing symbolic * links with their referents. Analogous to realpath(3). * * @return the canonical path for this path * @throws IOException if any symbolic link could not be resolved, or other * error occurred (for example, the path does not exist) */ public Path resolveSymbolicLinks() throws IOException { return fileSystem.resolveSymbolicLinks(this); } /** * Renames the file denoted by the current path to the location "target", not * following symbolic links. * *

Files cannot be atomically renamed across devices; copying is required. * Use {@link FileSystemUtils#copyFile} followed by {@link Path#delete}. * * @throws IOException if the rename failed for any reason */ public void renameTo(Path target) throws IOException { checkSameFilesystem(target); fileSystem.renameTo(this, target); } /** * Returns the size in bytes of the file denoted by the current path, * following symbolic links. * *

The size of a directory or special file is undefined and should not be used. * * @throws FileNotFoundException if the file denoted by the current path does * not exist * @throws IOException if the file's metadata could not be read, or some other * error occurred */ public long getFileSize() throws IOException, FileNotFoundException { return fileSystem.getFileSize(this, true); } /** * Returns the size in bytes of the file denoted by the current path. * *

The size of directory or special file is undefined. The size of a symbolic * link is the length of the name of its referent. * * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a * symbolic link, the link is deferenced until a file other than a * symbol link is found * @throws FileNotFoundException if the file denoted by the current path does * not exist * @throws IOException if the file's metadata could not be read, or some other * error occurred */ public long getFileSize(Symlinks followSymlinks) throws IOException, FileNotFoundException { return fileSystem.getFileSize(this, followSymlinks.toBoolean()); } /** * Deletes the file denoted by this path, not following symbolic links. * Returns normally iff the file doesn't exist after the call: true if this * call deleted the file, false if the file already didn't exist. Throws an * exception if the file could not be deleted for any reason. * * @return true iff the file was actually deleted by this call * @throws IOException if the deletion failed but the file was present prior * to the call */ public boolean delete() throws IOException { return fileSystem.delete(this); } /** * Returns the last modification time of the file, in milliseconds since the * UNIX epoch, of the file denoted by the current path, following symbolic * links. * *

Caveat: many filesystems store file times in seconds, so do not rely on * the millisecond precision. * * @throws IOException if the operation failed for any reason */ public long getLastModifiedTime() throws IOException { return fileSystem.getLastModifiedTime(this, true); } /** * Returns the last modification time of the file, in milliseconds since the * UNIX epoch, of the file denoted by the current path. * *

Caveat: many filesystems store file times in seconds, so do not rely on * the millisecond precision. * * @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a * symbolic link, the link is dereferenced until a file other than a * symbolic link is found * @throws IOException if the modification time for the file could not be * obtained for any reason */ public long getLastModifiedTime(Symlinks followSymlinks) throws IOException { return fileSystem.getLastModifiedTime(this, followSymlinks.toBoolean()); } /** * Sets the modification time of the file denoted by the current path. Follows * symbolic links. If newTime is -1, the current time according to the kernel * is used; this may differ from the JVM's clock. * *

Caveat: many filesystems store file times in seconds, so do not rely on * the millisecond precision. * * @param newTime time, in milliseconds since the UNIX epoch, or -1L, meaning * use the kernel's current time * @throws IOException if the modification time for the file could not be set * for any reason */ public void setLastModifiedTime(long newTime) throws IOException { fileSystem.setLastModifiedTime(this, newTime); } /** * Returns value of the given extended attribute name or null if attribute does not exist or * file system does not support extended attributes. Follows symlinks. */ public byte[] getxattr(String name) throws IOException { return fileSystem.getxattr(this, name); } /** * Returns the type of digest that may be returned by {@link #getFastDigest}, or {@code null} * if the filesystem doesn't support them. */ public String getFastDigestFunctionType() { return fileSystem.getFastDigestFunctionType(this); } /** * Gets a fast digest for the given path, or {@code null} if there isn't one available. The * digest should be suitable for detecting changes to the file. */ public byte[] getFastDigest() throws IOException { return fileSystem.getFastDigest(this); } /** * Returns the MD5 digest of the file denoted by the current path, following * symbolic links. * *

This method runs in O(n) time where n is the length of the file, but * certain implementations may be much faster than the worst case. * * @return a new 16-byte array containing the file's MD5 digest * @throws IOException if the MD5 digest could not be computed for any reason */ public byte[] getMD5Digest() throws IOException { return fileSystem.getMD5Digest(this); } /** * Returns the SHA1 digest of the file denoted by the current path, following * symbolic links. * *

This method runs in O(n) time where n is the length of the file, but * certain implementations may be much faster than the worst case. * * @return a new 20-byte array containing the file's SHA1 digest * @throws IOException if the SHA1 digest could not be computed for any reason */ public byte[] getSHA1Digest() throws IOException { return fileSystem.getSHA1Digest(this); } /** * Opens the file denoted by this path, following symbolic links, for reading, * and returns an input stream to it. * * @throws IOException if the file was not found or could not be opened for * reading */ public InputStream getInputStream() throws IOException { return fileSystem.getInputStream(this); } /** * Returns a java.io.File representation of this path. * *

Caveat: the result may be useless if this path's getFileSystem() is not * the UNIX filesystem. */ public File getPathFile() { return new File(getPathString()); } /** * Returns true if the file denoted by the current path, following symbolic * links, is writable for the current user. * * @throws FileNotFoundException if the file does not exist, a dangling * symbolic link was encountered, or the file's metadata could not be * read */ public boolean isWritable() throws IOException, FileNotFoundException { return fileSystem.isWritable(this); } /** * Sets the read permissions of the file denoted by the current path, * following symbolic links. Permissions apply to the current user. * * @param readable if true, the file is set to readable; otherwise the file is * made non-readable * @throws FileNotFoundException if the file does not exist * @throws IOException If the action cannot be taken (ie. permissions) */ public void setReadable(boolean readable) throws IOException, FileNotFoundException { fileSystem.setReadable(this, readable); } /** * Sets the write permissions of the file denoted by the current path, * following symbolic links. Permissions apply to the current user. * *

TODO(bazel-team): (2009) what about owner/group/others? * * @param writable if true, the file is set to writable; otherwise the file is * made non-writable * @throws FileNotFoundException if the file does not exist * @throws IOException If the action cannot be taken (ie. permissions) */ public void setWritable(boolean writable) throws IOException, FileNotFoundException { fileSystem.setWritable(this, writable); } /** * Returns true iff the file specified by the current path, following symbolic * links, is executable by the current user. * * @throws FileNotFoundException if the file does not exist or a dangling * symbolic link was encountered * @throws IOException if some other I/O error occurred */ public boolean isExecutable() throws IOException, FileNotFoundException { return fileSystem.isExecutable(this); } /** * Returns true iff the file specified by the current path, following symbolic * links, is readable by the current user. * * @throws FileNotFoundException if the file does not exist or a dangling * symbolic link was encountered * @throws IOException if some other I/O error occurred */ public boolean isReadable() throws IOException, FileNotFoundException { return fileSystem.isReadable(this); } /** * Sets the execute permission on the file specified by the current path, * following symbolic links. Permissions apply to the current user. * * @throws FileNotFoundException if the file does not exist or a dangling * symbolic link was encountered * @throws IOException if the metadata change failed, for example because of * permissions */ public void setExecutable(boolean executable) throws IOException, FileNotFoundException { fileSystem.setExecutable(this, executable); } /** * Sets the permissions on the file specified by the current path, following * symbolic links. If permission changes on this path's {@link FileSystem} are * slow (e.g. one syscall per change), this method should aim to be faster * than setting each permission individually. If this path's * {@link FileSystem} does not support group and others permissions, those * bits will be ignored. * * @throws FileNotFoundException if the file does not exist or a dangling * symbolic link was encountered * @throws IOException if the metadata change failed, for example because of * permissions */ public void chmod(int mode) throws IOException { fileSystem.chmod(this, mode); } /** * Compare Paths of the same file system using their PathFragments. * *

Paths from different filesystems will be compared using the identity * hash code of their respective filesystems. */ @Override public int compareTo(Path o) { // Fast-path. if (equals(o)) { return 0; } // If they are on different file systems, the file system decides the ordering. FileSystem otherFs = o.getFileSystem(); if (!fileSystem.equals(otherFs)) { int thisFileSystemHash = System.identityHashCode(fileSystem); int otherFileSystemHash = System.identityHashCode(otherFs); if (thisFileSystemHash < otherFileSystemHash) { return -1; } else if (thisFileSystemHash > otherFileSystemHash) { return 1; } else { // TODO(bazel-team): Add a name to every file system to be used here. return 0; } } // Equal file system, but different paths, because of the canonicalization. // We expect to often compare Paths that are very similar, for example for files in the same // directory. This can be done efficiently by going up segment by segment until we get the // identical path (canonicalization again), and then just compare the immediate child segments. // Overall this is much faster than creating PathFragment instances, and comparing those, which // requires us to always go up to the top-level directory and copy all segments into a new // string array. // This was previously showing up as a hotspot in a profile of globbing a large directory. Path a = this; Path b = o; int maxDepth = Math.min(a.depth, b.depth); while (a.depth > maxDepth) { a = a.getParentDirectory(); } while (b.depth > maxDepth) { b = b.getParentDirectory(); } // One is the child of the other. if (a.equals(b)) { // If a is the same as this, this.depth must be less than o.depth. return equals(a) ? -1 : 1; } Path previousa; Path previousb; do { previousa = a; previousb = b; a = a.getParentDirectory(); b = b.getParentDirectory(); } while (!a.equals(b)); // This has to happen eventually. return previousa.name.compareTo(previousb.name); } }