All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.nio.spi.s3.S3Path Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package software.amazon.nio.spi.s3;

import software.amazon.awssdk.services.s3.model.S3Object;

import java.io.File;
import java.io.IOError;
import java.net.URI;
import java.nio.file.*;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;

public class S3Path implements Path {

    public static final String PATH_SEPARATOR = "/";

    private final S3FileSystem fileSystem;
    private final PosixLikePathRepresentation pathRepresentation;

    private S3Path(S3FileSystem fileSystem, PosixLikePathRepresentation pathRepresentation){
        this.fileSystem = fileSystem;
        this.pathRepresentation = pathRepresentation;
    }

    /**
     * Construct a path using the same filesystem (bucket) as this path
     */
    private S3Path from(String path){
        return getPath(this.fileSystem, path);
    }

    /**
     * Construct a path from an S3 object in the bucket represented by the filesystem
     * @param fs the filesystem that holds (or will hold) the object represented by {@code s3Object}
     * @param s3Object the object
     * @return a new {@code S3Path}
     */
    public static S3Path getPath(S3FileSystem fs, S3Object s3Object){
        return getPath(fs, s3Object.key());
    }


    /**
     * Construct a Path in the parent FileSystem using the POSIX style.
     * The path string is assumed to follow the POSIX form
     * with the "root" of the bucket being represented by "/". The supplied path should not
     * be a URI. It should not start with the string "s3:". For example, if this S3FileSystem
     * represents "{@code s3://my-bucket}" then "{@code s3://my-bucket/foo.txt}" should be addressed by the path
     * "/foo.txt" or by a path relative to the current working directory following POSIX conventions.
     * Further, although folders or directories don't technically exist in S3
     * the presence of a directory is implicit if "{@code s3://my-bucket/someFolder/}" contains
     * objects and the Path to this folder is therefore valid.
     *
     * 

This library DOES NOT support S3 Paths that are not compliant with POSIX conventions. For example, * the URI {@code s3://my-bucket/../foo.txt} is legal in S3 but due to POSIX conventions it will be * unreachable through this API due to the special meaning of the .. directory alias in POSIX.

* * @param fsForBucket the filesystem for the bucket that holds this path * @param first the path string or initial part of the path string, may not be null. It may not be empty unless more is also null has zero length * @param more additional strings to be joined to form the path string * @throws InvalidPathException if the Path cannot be constructed * @return a new S3Path */ public static S3Path getPath(S3FileSystem fsForBucket, String first, String... more) { if(fsForBucket == null) throw new IllegalArgumentException("The S3FileSystem may not be null"); if(first == null ){ throw new IllegalArgumentException("first element of the path may not be null"); } first = first.trim(); if((first.isEmpty()) && !(more == null || more.length == 0)) throw new IllegalArgumentException("The first element of the path may not be empty when more exists"); if( first.startsWith(S3FileSystemProvider.SCHEME+":/")) { first = first.replaceFirst(S3FileSystemProvider.SCHEME+":/", ""); } return new S3Path(fsForBucket, PosixLikePathRepresentation.of(first, more)); } /** * Returns the file system that created this object. * * @return the file system that created this object */ @Override public S3FileSystem getFileSystem() { return fileSystem; } /** * The name of the S3 bucket that represents the root ("/") of this Path * @return the bucketName, equivalent to getFileSystem().bucketName() */ public String bucketName() { return fileSystem.bucketName(); } /** * Tells whether this path is absolute. * *

An absolute path is complete in that it doesn't need to be combined * with other path information in order to locate a file. * * @return {@code true} if, and only if, this path is absolute */ @Override public boolean isAbsolute() { return pathRepresentation.isAbsolute(); } /** * Is the path inferred to be an S3 directory? * @return true if the path can be inferrred to be a directory */ public boolean isDirectory() { return pathRepresentation.isDirectory(); } /** * If the path is absolute then returns the root of the path (e.g. "/") otherwise {@code null} * * @return a path representing the root component of this path, * or {@code null} */ @Override public S3Path getRoot() { return isAbsolute() ? new S3Path(fileSystem, PosixLikePathRepresentation.ROOT) : null; } /** * Returns the name of the file or directory denoted by this path as a * {@code Path} object. The file name is the farthest element from * the root in the directory hierarchy. * * @return a path representing the name of the file or directory, or * {@code null} if this path has zero elements */ @Override public S3Path getFileName() { final List elements = pathRepresentation.elements(); int size = elements.size(); if(size == 0) return null; if(pathRepresentation.hasTrailingSeparator()) { return from(elements.get(size -1) + PATH_SEPARATOR); } else { return from(elements.get(size -1)); } } /** * Returns the parent path, or {@code null} if this path does not * have a parent. * *

The parent of this path object consists of this path's root * component, if any, and each element in the path except for the * farthest from the root in the directory hierarchy. This method * does not access the file system; the path or its parent may not exist. * Furthermore, this method does not eliminate special names such as "." * and ".." that may be used in some implementations. On UNIX for example, * the parent of "{@code /a/b/c}" is "{@code /a/b}", and the parent of * {@code "x/y/.}" is "{@code x/y}". This method may be used with the {@link * #normalize normalize} method, to eliminate redundant names, for cases where * shell-like navigation is required. * *

If this path has one or more elements, and no root component, then * this method is equivalent to evaluating the expression: *

     * subpath(0, getNameCount()-1);
     * 
* * @return a path representing the path's parent */ @Override public S3Path getParent() { int size = pathRepresentation.elements().size(); if (this.equals(getRoot()) || size < 1) return null; if (pathRepresentation.isAbsolute() && size == 1) return getRoot(); return subpath(0, getNameCount()-1); } /** * Returns the number of name elements in the path. * * @return the number of elements in the path, or {@code 0} if this path * only represents a root component */ @Override public int getNameCount() { return pathRepresentation.elements().size(); } /** * Returns a name element of this path as a {@code Path} object. * *

The {@code index} parameter is the index of the name element to return. * The element that is closest to the root in the directory hierarchy * has the index {@code 0}. The element that is farthest from the root * has the index {@link #getNameCount count}{@code -1}. * * @param index the index of the element * @return the name element * @throws IllegalArgumentException if {@code index} is negative, {@code index} is greater than or * equal to the number of elements, or this path has zero name * elements */ @Override public S3Path getName(int index) { final List elements = pathRepresentation.elements(); if(index < 0 || index >= elements.size()) throw new IllegalArgumentException("index must be >= 0 and <= the number of path elements"); return subpath(index, index+1); } /** * Returns a relative {@code Path} that is a subsequence of the name * elements of this path. * *

The {@code beginIndex} and {@code endIndex} parameters specify the * subsequence of name elements. The name that is closest to the root * in the directory hierarchy has the index {@code 0}. The name that is * farthest from the root has the index {@link #getNameCount * count}{@code -1}. The returned {@code Path} object has the name elements * that begin at {@code beginIndex} and extend to the element at index {@code * endIndex-1}. * * @param beginIndex the index of the first element, inclusive * @param endIndex the index of the last element, exclusive * @return a new {@code Path} object that is a subsequence of the name * elements in this {@code Path} * @throws IllegalArgumentException if {@code beginIndex} is negative, or greater than or equal to * the number of elements. If {@code endIndex} is less than or * equal to {@code beginIndex}, or larger than the number of elements. */ @Override public S3Path subpath(int beginIndex, int endIndex) { final int size = pathRepresentation.elements().size(); if(beginIndex < 0) throw new IllegalArgumentException("begin index may not be < 0"); if(beginIndex >= size) throw new IllegalArgumentException("begin index may not be >= the number of path elements"); if(endIndex > size) throw new IllegalArgumentException("end index may not be > the number of path elements"); if(endIndex <= beginIndex) throw new IllegalArgumentException("end index may not be <= the begin index"); String path = String.join(PATH_SEPARATOR, pathRepresentation.elements().subList(beginIndex, endIndex)); if (this.isAbsolute() && beginIndex == 0) path = PATH_SEPARATOR+path; if (endIndex == size && !pathRepresentation.hasTrailingSeparator()) { return from(path); } else { return from(path+PATH_SEPARATOR); } } /** * Tests if this path starts with the given path. * *

This path starts with the given path if this path's root * component starts with the root component of the given path, * and this path starts with the same name elements as the given path. * If the given path has more name elements than this path then {@code false} * is returned. * *

If this path does * not have a root component and the given path has a root component then * this path does not start with the given path. * *

If the given path is associated with a different {@code FileSystem} (s3 bucket) * to this path then {@code false} is returned. * * @param other the given path * @return {@code true} if this path starts with the given path; otherwise * {@code false} */ @Override public boolean startsWith(Path other) { return this.equals(other) || this.fileSystem.equals(other.getFileSystem()) && this.isAbsolute() == other.isAbsolute() && this.getNameCount() >= other.getNameCount() && this.subpath(0, other.getNameCount()).equals(other); } /** * Tests if this path starts with a {@code Path}, constructed by converting * the given path string, in exactly the manner specified by the {@link * #startsWith(Path) startsWith(Path)} method. * * @param other the given path string * @return {@code true} if this path starts with the given path; otherwise * {@code false} * @throws InvalidPathException If the path string cannot be converted to a Path. */ @Override public boolean startsWith(String other) { return startsWith(from(other)); } /** * Tests if this path ends with the given path. * *

If the given path has N elements, and no root component, * and this path has N or more elements, then this path ends with * the given path if the last N elements of each path, starting at * the element farthest from the root, are equal. * *

If the given path has a root component then this path ends with the * given path if the root component of this path ends with the root * component of the given path, and the corresponding elements of both paths * are equal. If the two paths are equal then they can be said to end with each other. If this path * does not have a root component and the given path has a root component * then this path does not end with the given path. * *

If the given path is associated with a different {@code FileSystem} * to this path then {@code false} is returned. * * @param other the given path * @return {@code true} if this path ends with the given path; otherwise * {@code false} */ @Override public boolean endsWith(Path other) { return this.equals(other) || this.fileSystem == other.getFileSystem() && this.getNameCount() >= other.getNameCount() && this.subpath(this.getNameCount() - other.getNameCount(), this.getNameCount()).equals(other); } /** * Tests if this path ends with a {@code Path}, constructed by converting * the given path string, in exactly the manner specified by the {@link * #endsWith(Path) endsWith(Path)} method. On UNIX for example, the path * "{@code foo/bar}" ends with "{@code foo/bar}" and "{@code bar}". It does * not end with "{@code r}" or "{@code /bar}". Note that trailing separators * are not taken into account, and so invoking this method on the {@code * Path}"{@code foo/bar}" with the {@code String} "{@code bar/}" returns * {@code true}. * * @param other the given path string * @return {@code true} if this path ends with the given path; otherwise * {@code false} * @throws InvalidPathException If the path string cannot be converted to a Path. */ @Override public boolean endsWith(String other) { return endsWith(from(other)); } /** * Returns a path that is this path with redundant name elements eliminated. * All occurrences of "{@code .}" are considered redundant. If a "{@code ..}" is preceded by a * non-"{@code ..}" name then both names are considered redundant (the * process to identify such names is repeated until it is no longer * applicable). * *

This method does not access the file system; the path may not locate * a file that exists. Eliminating "{@code ..}" and a preceding name from a * path may result in the path that locates a different file than the original * path. This can arise when the preceding name is a symbolic link. * * @return the resulting path or this path if it does not contain * redundant name elements; an empty path is returned if this path * does have a root component and all name elements are redundant * @see #getParent * @see #toRealPath */ @Override public S3Path normalize() { if (pathRepresentation.isRoot()) { return this; } boolean directory = pathRepresentation.isDirectory(); final List elements = pathRepresentation.elements(); final LinkedList realElements = new LinkedList<>(); for (String element : elements) { if (element.equals(".")) continue; if (element.equals("..")){ if (!realElements.isEmpty()){ realElements.removeLast(); } continue; } if (directory) { realElements.addLast(element + "/"); } else { realElements.addLast(element); } } return S3Path.getPath(fileSystem, String.join(PATH_SEPARATOR, realElements)); } /** * Resolve the given path against this path. * *

If the {@code other} parameter is an {@link #isAbsolute() absolute} * path then this method trivially returns {@code other}. If {@code other} * is an empty path then this method trivially returns this path. * Otherwise, this method considers this path to be a directory and resolves * the given path against this path by * joining the given path to this path with the addition of a separator ('/') and returns a resulting path * that {@link #endsWith ends} with the given (other) path. * * @param other the path to resolve against this path * @return the resulting path * @throws ProviderMismatchException if {@code other} is {@code null} or if it is not an instance of {@code S3Path} * @throws IllegalArgumentException if {@code other} is NOT and instance of an {@code S3Path} * @see #relativize */ @Override public S3Path resolve(Path other) { if(!(other instanceof S3Path)) throw new ProviderMismatchException("a non s3 path cannot be resolved against and S3Path"); S3Path s3Other = (S3Path) other; if(!this.bucketName().equals(s3Other.bucketName())) throw new IllegalArgumentException("S3Paths cannot be resolved when they are from different buckets"); if (s3Other.isAbsolute()) return s3Other; if (s3Other.isEmpty()) return this; String concatenatedPath; if (!this.pathRepresentation.hasTrailingSeparator()) { concatenatedPath = this + PATH_SEPARATOR + s3Other; } else { concatenatedPath = this.toString() + s3Other; } return from(concatenatedPath); } /** * Converts a given path string to a {@code S3Path} and resolves it against * this {@code S3Path} in exactly the manner specified by the {@link * #resolve(Path) resolve} method. * * @param other the path string to resolve against this path * @return the resulting path * @throws InvalidPathException if the path string cannot be converted to a Path. * @see FileSystem#getPath */ @Override public S3Path resolve(String other) { return resolve(from(other)); } /** * Resolves the given path against this path's {@link #getParent parent} * path. This is useful where a file name needs to be replaced with * another file name. For example, suppose that the name separator is * "{@code /}" and a path represents "{@code dir1/dir2/foo}", then invoking * this method with the {@code Path} "{@code bar}" will result in the {@code * Path} "{@code dir1/dir2/bar}". If this path does not have a parent path, * or {@code other} is {@link #isAbsolute() absolute}, then this method * returns {@code other}. If {@code other} is an empty path then this method * returns this path's parent, or where this path doesn't have a parent, the * empty path. * * @param other the path to resolve against this path's parent * @return the resulting path * @see #resolve(Path) */ @Override public S3Path resolveSibling(Path other) { return getParent().resolve(other); } /** * Converts a given path string to a {@code Path} and resolves it against * this path's {@link #getParent parent} path in exactly the manner * specified by the {@link #resolveSibling(Path) resolveSibling} method. * * @param other the path string to resolve against this path's parent * @return the resulting path * @throws InvalidPathException if the path string cannot be converted to a Path. * @see FileSystem#getPath */ @Override public S3Path resolveSibling(String other) { return getParent().resolve(other); } /** * Constructs a relative path between this path and a given path. * *

Relativization is the inverse of {@link #resolve(Path) resolution}. * This method attempts to construct a {@link #isAbsolute relative} path * that when {@link #resolve(Path) resolved} against this path, yields a * path that locates the same file as the given path. For example, on UNIX, * if this path is {@code "/a/b"} and the given path is {@code "/a/b/c/d"} * then the resulting relative path would be {@code "c/d"}. Where this * path and the given path do not have a {@link #getRoot root} component, * then a relative path can be constructed. A relative path cannot be * constructed if only one of the paths have a root component. Where both * paths have a root component then it is implementation dependent if a * relative path can be constructed. If this path and the given path are * {@link #equals equal} then an empty path is returned. * * @param other the path to relativize against this path * @return the resulting relative path, or an empty path if both paths are * equal * @throws IllegalArgumentException if {@code other} is not a {@code Path} that can be relativized * against this path */ @Override public S3Path relativize(Path other) { if(!(other instanceof S3Path)) throw new IllegalArgumentException("path is not an S3Path"); if(this.equals(other)) return from(""); if(this.isAbsolute() != other.isAbsolute()) throw new IllegalArgumentException("to obtain a relative path both must be absolute or both must be relative"); if(!Objects.equals(this.bucketName(), ((S3Path) other).bucketName())) throw new IllegalArgumentException("cannot relativize S3Paths from different buckets"); S3Path otherPath = (S3Path) other; if(this.isEmpty()) return otherPath; int nameCount = this.getNameCount(); int otherNameCount = other.getNameCount(); int limit = Math.min(nameCount, otherNameCount); int differenceCount = getDifferenceCount(other, limit); int parentDirCount = nameCount - differenceCount; if (differenceCount < otherNameCount) { return getRelativePathFromDifference(otherPath, otherNameCount, differenceCount, parentDirCount); } char[] relativePath = new char[parentDirCount*3 - 1]; int index = 0; while (parentDirCount > 0) { relativePath[index++] = '.'; relativePath[index++] = '.'; if (parentDirCount > 1) relativePath[index++] = '/'; parentDirCount--; } return new S3Path(getFileSystem(), new PosixLikePathRepresentation(relativePath)); } private S3Path getRelativePathFromDifference(S3Path otherPath, int otherNameCount, int differenceCount, int parentDirCount) { Objects.requireNonNull(otherPath); S3Path remainingSubPath = otherPath.subpath(differenceCount, otherNameCount); if (parentDirCount == 0) return remainingSubPath; // we need to pop up some directories (each of which needs three characters ../) then append the remaining sub-path int relativePathSize = parentDirCount * 3 + remainingSubPath.pathRepresentation.toString().length(); if (otherPath.isEmpty()) relativePathSize--; char[] relativePath = new char[relativePathSize]; int index = 0; while (parentDirCount > 0) { relativePath[index++] = '.'; relativePath[index++] = '.'; if (otherPath.isEmpty()) { if (parentDirCount > 1) relativePath[index++] = '/'; } else { relativePath[index++] = '/'; } parentDirCount--; } System.arraycopy(remainingSubPath.pathRepresentation.chars(), 0, relativePath, index, remainingSubPath.pathRepresentation.chars().length); return new S3Path(getFileSystem(), new PosixLikePathRepresentation(relativePath)); } private int getDifferenceCount(Path other, int limit) { int i = 0; while (i < limit) { if (!this.getName(i).equals(other.getName(i))) break; i++; } return i; } private boolean isEmpty(){ return pathRepresentation.toString().isEmpty(); } /** * Returns a URI to represent this path. * *

This method constructs an absolute and normalized {@link URI} with a {@link * URI#getScheme() scheme} equal to the URI scheme that identifies the * provider (s3). * * @return the URI representing this path * @throws IOError if an I/O error occurs obtaining the absolute path, or where a * file system is constructed to access the contents of a file as * a file system, and the URI of the enclosing file system cannot be * obtained * @throws SecurityException In the case of the default provider, and a security manager * is installed, the {@link #toAbsolutePath toAbsolutePath} method * throws a security exception. */ @Override public URI toUri() { return URI.create( fileSystem.provider().getScheme() + "://" + bucketName() + this.toAbsolutePath().toRealPath(NOFOLLOW_LINKS)); } /** * Returns a {@code Path} object representing the absolute path of this * path. * *

If this path is already {@link Path#isAbsolute absolute} then this * method simply returns this path. Otherwise, this method resolves the path * by resolving the path against the root (the top level of the bucket). The resulting path may contain redundancies * and may point to a non-existent location. * * @return a {@code Path} object representing the absolute path */ @Override public S3Path toAbsolutePath() { if (isAbsolute()) return this; return new S3Path(fileSystem, PosixLikePathRepresentation.of(PATH_SEPARATOR, pathRepresentation.toString())); } /** * Returns the real path of an existing file. * *

If this path is relative then its absolute path is first obtained, * as if by invoking the {@link #toAbsolutePath toAbsolutePath} method. * When deriving the real path, and a * "{@code ..}" (or equivalent) is preceded by a non-"{@code ..}" name then * an implementation will cause both names to be removed. * * @param options options indicating how symbolic links are handled. S3 has no links so this will be ignored. * @return an absolute path represent the real path of the file * located by this object */ @Override public S3Path toRealPath(LinkOption... options) { S3Path p = this; if(!isAbsolute()) p = toAbsolutePath(); return S3Path.getPath(fileSystem, PATH_SEPARATOR, p.normalize().toString()); } /** * S3 Objects cannot be represented in the local file system * @throws UnsupportedOperationException always */ @Override public File toFile() { throw new UnsupportedOperationException("S3 Objects cannot be represented in the local (default) file system"); } /** * Currently not implemented * @throws UnsupportedOperationException always */ @Override public WatchKey register(WatchService watcher, WatchEvent.Kind[] events, WatchEvent.Modifier... modifiers) throws UnsupportedOperationException { throw new UnsupportedOperationException("This method is not yet supported. Please raise a feature request describing your use case"); } /** * Currently not implemented * @throws UnsupportedOperationException always */ @Override public WatchKey register(WatchService watcher, WatchEvent.Kind... events) throws UnsupportedOperationException { throw new UnsupportedOperationException("This method is not yet supported. Please raise a feature request describing your use case"); } /** * Returns an iterator over the name elements of this path. * *

The first element returned by the iterator represents the name * element that is closest to the root in the directory hierarchy, the * second element is the next closest, and so on. The last element returned * is the name of the file or directory denoted by this path. The {@link * #getRoot root} component, if present, is not returned by the iterator. * * @return an iterator over the name elements of this path. */ @Override public Iterator iterator() { return new S3PathIterator(pathRepresentation.elements().iterator(), pathRepresentation.isAbsolute(), pathRepresentation.hasTrailingSeparator()); } /** * Compares two abstract paths lexicographically. The ordering defined by * this method is provider specific, and in the case of the default * provider, platform specific. This method does not access the file system * and neither file is required to exist. * *

This method may not be used to compare paths that are associated * with different file system providers. * * @param other the path compared to this path. * @return zero if the argument is {@link #equals equal} to this path, a * value less than zero if this path is lexicographically less than * the argument, or a value greater than zero if this path is * lexicographically greater than the argument * @throws ClassCastException if the paths are associated with different providers */ @Override public int compareTo(Path other) { if(!(other instanceof S3Path)) throw new ClassCastException("compared paths must be from the same provider"); S3Path o = (S3Path) other; if(o.fileSystem != this.fileSystem) throw new ClassCastException("compared S3 paths must be from the same bucket"); return this.toRealPath(NOFOLLOW_LINKS).toString().compareTo( o.toRealPath(NOFOLLOW_LINKS).toString()); } /** * Tests this path for equality with the given object. * * {@code true} if {@code other} is also an {@code S3Path} from the same bucket and the two paths have the same * real path. * @param other the object to which this object is to be compared * @return {@code true} if, and only if, the given object is a {@code Path} * that is identical to this {@code Path} */ @Override public boolean equals(Object other) { if (this == other) return true; return other instanceof S3Path && Objects.equals(((S3Path) other).bucketName(), this.bucketName()) && Objects.equals(((S3Path) other).toRealPath(NOFOLLOW_LINKS).pathRepresentation, this.toRealPath(NOFOLLOW_LINKS).pathRepresentation); } /** * Computes a hash code for this path. * *

The hash code is based upon the components of the path, and * satisfies the general contract of the {@link Object#hashCode * Object.hashCode} method. * * @return the hash-code value for this path */ @Override public int hashCode() { return toRealPath(NOFOLLOW_LINKS).pathRepresentation.hashCode(); } /** * Returns the string representation of this path. * * @return the string representation of this path */ @Override public String toString() { return pathRepresentation.toString(); } /** * The key of the object for S3. Essentially the "real path" with the "/" prefix removed. * @return the key */ public String getKey(){ if(isEmpty()) return ""; return toRealPath(NOFOLLOW_LINKS).toString().substring(1); } private final class S3PathIterator implements Iterator { private final Iterator delegate; boolean first; boolean isAbsolute; boolean hasTrailingSeparator; public S3PathIterator(Iterator delegate, boolean isAbsolute, boolean hasTrailingSeparator){ this.delegate = delegate; this.isAbsolute = isAbsolute; this.hasTrailingSeparator = hasTrailingSeparator; first = true; } @Override public Path next() { String pathString = delegate.next(); if(isAbsolute() && first){ first = false; pathString = PATH_SEPARATOR+pathString; if (!hasNext() && hasTrailingSeparator) { pathString = pathString+PATH_SEPARATOR; } } if(hasNext() || hasTrailingSeparator) { pathString = pathString+PATH_SEPARATOR; } return from(pathString); } @Override public boolean hasNext() { return delegate.hasNext(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy