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

com.signalfx.shaded.jetty.util.resource.PathResource Maven / Gradle / Ivy

The newest version!
//
//  ========================================================================
//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package com.signalfx.shaded.jetty.util.resource;

import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.List;

import com.signalfx.shaded.jetty.util.BufferUtil;
import com.signalfx.shaded.jetty.util.IO;
import com.signalfx.shaded.jetty.util.StringUtil;
import com.signalfx.shaded.jetty.util.URIUtil;
import com.signalfx.shaded.jetty.util.log.Log;
import com.signalfx.shaded.jetty.util.log.Logger;

/**
 * Java NIO Path equivalent of FileResource.
 */
public class PathResource extends Resource
{
    private static final Logger LOG = Log.getLogger(PathResource.class);
    private static final LinkOption[] NO_FOLLOW_LINKS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS};
    private static final LinkOption[] FOLLOW_LINKS = new LinkOption[]{};

    private final Path path;
    private final Path alias;
    private final URI uri;
    private final boolean belongsToDefaultFileSystem;

    private Path checkAliasPath()
    {
        Path abs = path;

        /* Catch situation where the Path class has already normalized
         * the URI eg. input path "aa./foo.txt"
         * from an #addPath(String) is normalized away during
         * the creation of a Path object reference.
         * If the URI is different then the Path.toUri() then
         * we will just use the original URI to construct the
         * alias reference Path.
         */
        if (!URIUtil.equalsIgnoreEncodings(uri, path.toUri()))
        {
            try
            {
                return Paths.get(uri).toRealPath(FOLLOW_LINKS);
            }
            catch (IOException ignored)
            {
                // If the toRealPath() call fails, then let
                // the alias checking routines continue on
                // to other techniques.
                LOG.ignore(ignored);
            }
        }

        if (!abs.isAbsolute())
            abs = path.toAbsolutePath();

        // Any normalization difference means it's an alias,
        // and we don't want to bother further to follow
        // symlinks as it's an alias anyway.
        Path normal = path.normalize();
        if (!isSameName(abs, normal))
            return normal;

        try
        {
            if (Files.isSymbolicLink(path))
                return path.getParent().resolve(Files.readSymbolicLink(path));
            if (Files.exists(path))
            {
                Path real = abs.toRealPath(FOLLOW_LINKS);
                if (!isSameName(abs, real))
                    return real;
            }
        }
        catch (IOException e)
        {
            LOG.ignore(e);
        }
        catch (Exception e)
        {
            LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(), path);
        }
        return null;
    }

    /**
     * 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 File object. *

* An invocation of this convenience constructor of the form. *

*
     * new PathResource(file);
     * 
*

* behaves in exactly the same way as the expression *

*
     * new PathResource(file.toPath());
     * 
* * @param file the file to use */ public PathResource(File file) { this(file.toPath()); } /** * Construct a new PathResource from a Path object. * * @param path the path to use */ public PathResource(Path path) { Path absPath = path; try { absPath = path.toRealPath(NO_FOLLOW_LINKS); } catch (IOError | IOException e) { // Not able to resolve real/canonical path from provided path // This could be due to a glob reference, or a reference // to a path that doesn't exist (yet) if (LOG.isDebugEnabled()) LOG.debug("Unable to get real/canonical path for {}", path, e); } this.path = absPath; assertValidPath(path); this.uri = this.path.toUri(); this.alias = checkAliasPath(); this.belongsToDefaultFileSystem = this.path.getFileSystem() == FileSystems.getDefault(); } /** * Construct a new PathResource from a parent PathResource * and child sub path * * @param parent the parent path resource * @param childPath the child sub path */ private PathResource(PathResource parent, String childPath) { // Calculate the URI and the path separately, so that any aliasing done by // FileSystem.getPath(path,childPath) is visible as a difference to the URI // obtained via URIUtil.addDecodedPath(uri,childPath) // The checkAliasPath normalization checks will only work correctly if the getPath implementation here does not normalize. this.path = parent.path.getFileSystem().getPath(parent.path.toString(), childPath); if (isDirectory() && !childPath.endsWith("/")) childPath += "/"; this.uri = URIUtil.addPath(parent.uri, childPath); this.alias = checkAliasPath(); this.belongsToDefaultFileSystem = this.path.getFileSystem() == FileSystems.getDefault(); } /** * 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. * @throws IOException if unable to construct the PathResource from the URI. */ public PathResource(URI uri) throws IOException { if (!uri.isAbsolute()) { throw new IllegalArgumentException("not an absolute uri"); } if (!uri.getScheme().equalsIgnoreCase("file")) { throw new IllegalArgumentException("not file: scheme"); } Path path; try { path = Paths.get(uri); } catch (IllegalArgumentException e) { throw e; } catch (Exception e) { LOG.ignore(e); throw new IOException("Unable to build Path from: " + uri, e); } this.path = path.toAbsolutePath(); this.uri = path.toUri(); this.alias = checkAliasPath(); this.belongsToDefaultFileSystem = this.path.getFileSystem() == FileSystems.getDefault(); } /** * Create a new PathResource from a provided URL object. *

* An invocation of this convenience constructor of the form. *

*
     * new PathResource(url);
     * 
*

* behaves in exactly the same way as the expression *

*
     * new PathResource(url.toURI());
     * 
* * @param url the url to attempt to create PathResource from * @throws IOException if URL doesn't point to a location that can be transformed to a PathResource * @throws URISyntaxException if the provided URL was malformed */ public PathResource(URL url) throws IOException, URISyntaxException { this(url.toURI()); } @Override public boolean isSame(Resource resource) { try { if (resource instanceof PathResource) { Path path = ((PathResource)resource).getPath(); return Files.isSameFile(getPath(), path); } if (resource instanceof FileResource) { Path path = ((FileResource)resource).getFile().toPath(); return Files.isSameFile(getPath(), path); } } catch (IOException e) { if (LOG.isDebugEnabled()) LOG.debug("ignored", e); } return false; } @Override public Resource addPath(final String subPath) throws IOException { // Check that the path is within the root, // but use the original path to create the // resource, to preserve aliasing. if (URIUtil.canonicalPath(subPath) == null) throw new MalformedURLException(subPath); if ("/".equals(subPath)) return this; // Sub-paths are always under PathResource // compensate for input sub-paths like "/subdir" // where default resolve behavior would be // to treat that like an absolute path return new PathResource(this, subPath); } private void assertValidPath(Path path) { // TODO merged from 9.2, check if necessary String str = path.toString(); int idx = StringUtil.indexOfControlChars(str); if (idx >= 0) { throw new InvalidPathException(str, "Invalid Character at index " + idx); } } @Override public void close() { // not applicable for FileSytem / Path } @Override public boolean delete() throws SecurityException { try { return Files.deleteIfExists(path); } catch (IOException e) { LOG.ignore(e); return false; } } @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; if (path == null) { return other.path == null; } else return path.equals(other.path); } @Override public boolean exists() { return Files.exists(path, NO_FOLLOW_LINKS); } @Override public File getFile() throws IOException { if (!belongsToDefaultFileSystem) return null; return path.toFile(); } /** * @return the {@link Path} of the resource */ public Path getPath() { return path; } @Override public InputStream getInputStream() throws IOException { return Files.newInputStream(path, StandardOpenOption.READ); } @Override public String getName() { return path.toAbsolutePath().toString(); } @Override public ReadableByteChannel getReadableByteChannel() throws IOException { return newSeekableByteChannel(); } public SeekableByteChannel newSeekableByteChannel() throws IOException { return Files.newByteChannel(path, StandardOpenOption.READ); } @Override public URI getURI() { return this.uri; } @Override public URL getURL() { try { return path.toUri().toURL(); } catch (MalformedURLException e) { return null; } } @Override public int hashCode() { final int prime = 31; int result = 1; result = (prime * result) + ((path == null) ? 0 : path.hashCode()); return result; } @Override public boolean isContainedIn(Resource r) throws MalformedURLException { // not applicable for FileSystem / path return false; } @Override public boolean isDirectory() { return Files.isDirectory(path, FOLLOW_LINKS); } @Override public long lastModified() { try { FileTime ft = Files.getLastModifiedTime(path, FOLLOW_LINKS); return ft.toMillis(); } catch (IOException e) { LOG.ignore(e); return 0; } } @Override public long length() { try { return Files.size(path); } catch (IOException e) { // in case of error, use File.length logic of 0L return 0L; } } @Override public boolean isAlias() { return this.alias != null; } /** * The Alias as a Path. *

* Note: this cannot return the alias as a DIFFERENT path in 100% of situations, * due to Java's internal Path/File normalization. *

* * @return the alias as a path. */ public Path getAliasPath() { return this.alias; } @Override public URI getAlias() { return this.alias == null ? null : this.alias.toUri(); } @Override public String[] list() { try (DirectoryStream dir = Files.newDirectoryStream(path)) { List entries = new ArrayList<>(); for (Path entry : dir) { String name = entry.getFileName().toString(); if (Files.isDirectory(entry)) { name += "/"; } entries.add(name); } int size = entries.size(); return entries.toArray(new String[size]); } catch (DirectoryIteratorException | IOException e) { LOG.debug(e); } return null; } @Override public boolean renameTo(Resource dest) throws SecurityException { if (dest instanceof PathResource) { PathResource destRes = (PathResource)dest; try { Path result = Files.move(path, destRes.path); return Files.exists(result, NO_FOLLOW_LINKS); } catch (IOException e) { LOG.ignore(e); return false; } } else { return false; } } @Override public void copyTo(File destination) throws IOException { if (isDirectory()) { IO.copyDir(this.path.toFile(), destination); } else { Files.copy(this.path, destination.toPath()); } } /** * @param outputStream the output stream to write to * @param start First byte to write * @param count Bytes to write or -1 for all of them. * @throws IOException if unable to copy the Resource to the output */ @Override public void writeTo(OutputStream outputStream, long start, long count) throws IOException { long length = count; if (count < 0) { length = Files.size(path) - start; } try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) { ByteBuffer buffer = BufferUtil.allocate(IO.bufferSize); skipTo(channel, buffer, start); // copy from channel to output stream long readTotal = 0; while (readTotal < length) { BufferUtil.clearToFill(buffer); int size = (int)Math.min(IO.bufferSize, length - readTotal); buffer.limit(size); int readLen = channel.read(buffer); BufferUtil.flipToFlush(buffer, 0); BufferUtil.writeTo(buffer, outputStream); readTotal += readLen; } } } private void skipTo(SeekableByteChannel channel, ByteBuffer buffer, long skipTo) throws IOException { try { if (channel.position() != skipTo) { channel.position(skipTo); } } catch (UnsupportedOperationException e) { final int NO_PROGRESS_LIMIT = 3; if (skipTo > 0) { long pos = 0; long readLen; int noProgressLoopLimit = NO_PROGRESS_LIMIT; // loop till we reach desired point, break out on lack of progress. while (noProgressLoopLimit > 0 && pos < skipTo) { BufferUtil.clearToFill(buffer); int len = (int)Math.min(IO.bufferSize, (skipTo - pos)); buffer.limit(len); readLen = channel.read(buffer); if (readLen == 0) { noProgressLoopLimit--; } else if (readLen > 0) { pos += readLen; noProgressLoopLimit = NO_PROGRESS_LIMIT; } else { // negative values means the stream was closed or reached EOF // either way, we've hit a state where we can no longer // fulfill the requested range write. throw new IOException("EOF reached before SeekableByteChannel skip destination"); } } if (noProgressLoopLimit <= 0) { throw new IOException("No progress made to reach SeekableByteChannel skip position " + skipTo); } } } } @Override public String toString() { return this.uri.toASCIIString(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy