org.eclipse.jetty.util.resource.PathResource Maven / Gradle / Ivy
Show all versions of jetty-util Show documentation
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util.resource;
import java.io.IOException;
import java.net.URI;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Java NIO Path Resource.
*/
public class PathResource extends Resource
{
private static final Logger LOG = LoggerFactory.getLogger(PathResource.class);
/**
* @deprecated Using ResourceFactoryInternals.isSupported() instead.
*/
@Deprecated(since = "12.0.4", forRemoval = true)
public static Index SUPPORTED_SCHEMES = new Index.Builder().build();
// The path object represented by this instance
private final Path path;
// The as-requested URI for this path object
private final URI uri;
// True / False to indicate if this is an alias of something else, or null if the alias hasn't been resolved
private Boolean alias;
// The Path representing the real-path of this PathResource instance. (populated during alias checking)
private Path realPath;
/**
* Test if the paths are the same name.
*
*
* If the real path is not the same as the absolute path
* then we know that the real path is the alias for the
* provided path.
*
*
*
* For OS's that are case insensitive, this should
* return the real (on-disk / case correct) version
* of the path.
*
*
*
* We have to be careful on Windows and OSX.
*
*
*
* Assume we have the following scenario:
*
*
*
* Path a = new File("foo").toPath();
* Files.createFile(a);
* Path b = new File("FOO").toPath();
*
*
*
* There now exists a file called {@code foo} on disk.
* Using Windows or OSX, with a Path reference of
* {@code FOO}, {@code Foo}, {@code fOO}, etc.. means the following
*
*
*
* | OSX | Windows | Linux
* -----------------------+---------+------------+---------
* Files.exists(a) | True | True | True
* Files.exists(b) | True | True | False
* Files.isSameFile(a,b) | True | True | False
* a.equals(b) | False | True | False
*
*
*
* See the javadoc for Path.equals() for details about this FileSystem
* behavior difference
*
*
*
* We also cannot rely on a.compareTo(b) as this is roughly equivalent
* in implementation to a.equals(b)
*
*/
public static boolean isSameName(Path pathA, Path pathB)
{
int aCount = pathA.getNameCount();
int bCount = pathB.getNameCount();
if (aCount != bCount)
{
// different number of segments
return false;
}
// compare each segment of path, backwards
for (int i = bCount; i-- > 0; )
{
if (!pathA.getName(i).toString().equals(pathB.getName(i).toString()))
{
return false;
}
}
return true;
}
/**
* Construct a new PathResource from a URI object.
*
* Must be an absolute URI using the file
scheme.
*
* @param uri the URI to build this PathResource from.
*/
PathResource(URI uri)
{
this(uri, false);
}
PathResource(URI uri, boolean bypassAllowedSchemeCheck)
{
this(Paths.get(uri), uri, bypassAllowedSchemeCheck);
}
PathResource(Path path)
{
this(path, path.toUri(), true);
}
/**
* Create a PathResource.
*
* @param path the Path object
* @param uri the as-requested URI for the resource
* @param bypassAllowedSchemeCheck true to bypass the allowed schemes check
*/
PathResource(Path path, URI uri, boolean bypassAllowedSchemeCheck)
{
if (!uri.isAbsolute())
throw new IllegalArgumentException("not an absolute uri: " + uri);
if (!bypassAllowedSchemeCheck && !ResourceFactoryInternals.isSupported(uri.getScheme()))
throw new IllegalArgumentException("not an allowed scheme: " + uri);
if (Files.isDirectory(path))
{
String uriString = uri.toASCIIString();
if (!uriString.endsWith("/"))
uri = URIUtil.correctURI(URI.create(uriString + "/"));
}
this.path = path;
this.uri = uri;
}
@Override
public boolean exists()
{
if (alias == null)
{
// no alias check performed
return Files.exists(path);
}
else
{
if (realPath == null)
return false;
return Files.exists(realPath);
}
}
@Override
public Path getPath()
{
return path;
}
@Override
public boolean contains(Resource other)
{
if (other == null)
return false;
Path thisPath = getPath();
if (thisPath == null)
throw new UnsupportedOperationException("Resources without a Path must implement contains");
Path otherPath = other.getPath();
return otherPath != null &&
otherPath.getFileSystem().equals(thisPath.getFileSystem()) &&
otherPath.startsWith(thisPath);
}
public Path getRealPath()
{
resolveAlias();
return realPath;
}
@Override
public URI getRealURI()
{
Path realPath = getRealPath();
return (realPath == null) ? null : realPath.toUri();
}
public List list()
{
if (!isDirectory())
return List.of(); // empty
try (Stream dirStream = Files.list(getPath()))
{
return dirStream.map(PathResource::new).collect(Collectors.toCollection(ArrayList::new));
}
catch (DirectoryIteratorException e)
{
LOG.debug("Directory list failure", e);
}
catch (IOException e)
{
LOG.debug("Directory list access failure", e);
}
return List.of(); // empty
}
@Override
public boolean isAlias()
{
resolveAlias();
return alias != null && alias;
}
@Override
public String getName()
{
return path.toAbsolutePath().toString();
}
@Override
public String getFileName()
{
Path fn = path.getFileName();
if (fn == null) // if path has no segments (eg "/")
return "";
return fn.toString();
}
@Override
public URI getURI()
{
return this.uri;
}
@Override
public Resource resolve(String subUriPath)
{
// Check that the path is within the root,
// but use the original path to create the
// resource, to preserve aliasing.
if (URIUtil.isNotNormalWithinSelf(subUriPath))
throw new IllegalArgumentException(subUriPath);
if ("/".equals(subUriPath))
return this;
URI uri = getURI();
URI resolvedUri = URIUtil.addPath(uri, subUriPath);
Path path = Paths.get(resolvedUri);
return newResource(path, resolvedUri);
}
/**
* Internal override for creating a new PathResource.
* Used by MountedPathResource (eg)
*/
protected Resource newResource(Path path, URI uri)
{
return new PathResource(path, uri, true);
}
@Override
public boolean isDirectory()
{
return Files.isDirectory(getPath());
}
@Override
public boolean isReadable()
{
return Files.isReadable(getPath());
}
@Override
public Instant lastModified()
{
Path path = getPath();
if (path == null)
return Instant.EPOCH;
if (!Files.exists(path))
return Instant.EPOCH;
try
{
FileTime ft = Files.getLastModifiedTime(path, LinkOption.NOFOLLOW_LINKS);
return ft.toInstant();
}
catch (IOException e)
{
LOG.trace("IGNORED", e);
return Instant.EPOCH;
}
}
@Override
public long length()
{
try
{
return Files.size(getPath());
}
catch (IOException e)
{
return -1L;
}
}
/**
*
* Perform a check of the original Path and as-requested URI to determine
* if this resource is an alias to another name/location.
*
*
*
* Alias Check Logic
*
*
* path
* realPath
* uri-as-requested
* uri-from-realPath
* alias
*
*
*
*
* C:/temp/aa./foo.txt
* C:/temp/aa/foo.txt
* file:///C:/temp/aa./foo.txt
* file:///C:/temp/aa./foo.txt
* true
*
*
* /tmp/foo-symlink
* /tmp/bar.txt
* file:///tmp/foo-symlink
* file:///tmp/bar.txt
* true
*
*
* C:/temp/aa.txt
* C:/temp/AA.txt
* file:///C:/temp/aa.txt
* file:///C:/temp/AA.txt
* true
*
*
* /tmp/bar-exists/../foo.txt
* /tmp/foo.txt
* file:///tmp/bar-exists/../foo.txt
* file:///tmp/foo.txt
* true
*
*
* /tmp/doesnt-exist.txt
* null (does not exist)
* file:///tmp/doesnt-exist.txt
* null (does not exist)
* false
*
*
* /tmp/doesnt-exist/../foo.txt
* null (intermediate does not exist)
* file:///tmp/doesnt-exist/../foo.txt
* null (intermediate does not exist)
* false
*
*
* /var/protected/config.xml
* null (no permissions)
* file:///var/protected/config.xml
* null (no permission)
* false
*
*
* /tmp/foo-symlink
* null (broken symlink, doesn't point to anything)
* file:///tmp/foo-symlink
* null (broken symlink, doesn't point to anything)
* false
*
*
* C:/temp/cannot:be:referenced
* null (illegal filename)
* file:///C:/temp/cannot:be:referenced
* null (illegal filename)
* false
*
*
*
*/
private void resolveAlias()
{
if (alias == null)
{
try
{
// Default behavior is to follow symlinks.
// We don't want to use the NO_FOLLOW_LINKS parameter as that takes this call from
// being filesystem aware, and using FileSystem specific techniques to find
// the real file, to being done in-API (which doesn't work reliably on
// filesystems that have different names for the same file.
// eg: case-insensitive file systems, unicode name normalization,
// alternate names, etc)
// We also don't want to use Path.normalize() here as that eliminates
// the knowledge of what directories are being navigated through.
realPath = path.toRealPath();
}
catch (Exception e)
{
if (e instanceof IOException)
LOG.trace("IGNORED", e);
else
LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(), path);
// Not possible to serve this resource.
// - This resource doesn't exist.
// - No access rights to this resource.
// - Unable to read the file or directory.
// - Navigation segments (eg: "foo/../test.txt") would go through something that doesn't exist, or not accessible.
// - FileSystem doesn't support toRealPath.
return;
}
/* If the path and realPath are the same, also check
* The as-requested URI as it will represent what was
* URI created this PathResource.
* e.g. the input of `resolve("aa./foo.txt")
* on windows would resolve the path, but the Path.toUri() would
* not always show this extension-less access.
* The as-requested URI will retain this extra '.' and be used
* to evaluate if the realPath.toUri() is the same as the as-requested URI.
*
* // On Windows
* PathResource resource = PathResource("C:/temp");
* PathResource child = resource.resolve("aa./foo.txt");
* child.exists() == true
* child.isAlias() == true
* child.toUri() == "file:///C:/temp/aa./foo.txt"
* child.getPath().toUri() == "file:///C:/temp/aa/foo.txt"
* child.getRealURI() == "file:///C:/temp/aa/foo.txt"
*/
alias = !isSameName(path, realPath) || !Objects.equals(uri, toUri(realPath));
}
}
/**
* Ensure Path to URI is sane when it returns a directory reference.
*
*
* This is different than {@link Path#toUri()} in that not
* all FileSystems seem to put the trailing slash on a directory
* reference in the URI.
*
*
* @param path the path to convert to URI
* @return the appropriate URI for the path
*/
protected URI toUri(Path path)
{
URI pathUri = path.toUri();
String rawUri = pathUri.toASCIIString();
if (Files.isDirectory(path) && !rawUri.endsWith("/"))
{
return URI.create(rawUri + '/');
}
return pathUri;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PathResource other = (PathResource)obj;
return Objects.equals(path, other.path) && Objects.equals(uri, other.uri);
}
@Override
public int hashCode()
{
return Objects.hash(path, uri);
}
@Override
public String toString()
{
return this.uri.toASCIIString();
}
}