/* * The MIT License * * Copyright 2013 Jesse Glick. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package jenkins.util; import hudson.FilePath; import hudson.model.DirectoryBrowserSupport; import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; import hudson.util.DirScanner; import hudson.util.FileVisitor; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.net.URI; import java.nio.file.InvalidPathException; import java.nio.file.LinkOption; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nonnull; import jenkins.MasterToSlaveFileCallable; /** * Abstraction over {@link File}, {@link FilePath}, or other items such as network resources or ZIP entries. * Assumed to be read-only and makes very limited assumptions, just enough to display content and traverse directories. * * <p> * To obtain a {@link VirtualFile} representation for an existing file, use {@link #forFile(File)} or {@link FilePath#toVirtualFile()} * * <h2>How are VirtualFile and FilePath different?</h2> * <p> * FilePath abstracts away {@link File}s on machines that are connected over {@link Channel}, whereas * {@link VirtualFile} makes no assumption about where the actual files are, or whether there really exists * {@link File}s somewhere. This makes VirtualFile more abstract. * * @see DirectoryBrowserSupport * @see FilePath * @since 1.532 */ public abstract class VirtualFile implements Comparable<VirtualFile>, Serializable { /** * Gets the base name, meaning just the last portion of the path name without any * directories. * * For a “root directory” this may be the empty string. * @return a simple name (no slashes) */ public abstract @Nonnull String getName(); /** * Gets a URI. * Should at least uniquely identify this virtual file within its root, but not necessarily globally. * @return a URI (need not be absolute) */ public abstract URI toURI(); /** * Gets the parent file. * Need only operate within the originally given root. * @return the parent */ public abstract VirtualFile getParent(); /** * Checks whether this file exists and is a directory. * @return true if it is a directory, false if a file or nonexistent * @throws IOException in case checking status failed */ public abstract boolean isDirectory() throws IOException; /** * Checks whether this file exists and is a plain file. * @return true if it is a file, false if a directory or nonexistent * @throws IOException in case checking status failed */ public abstract boolean isFile() throws IOException; /** * Checks whether this file exists. * @return true if it is a plain file or directory, false if nonexistent * @throws IOException in case checking status failed */ public abstract boolean exists() throws IOException; /** * Lists children of this directory. * @return a list of children (files and subdirectories); empty for a file or nonexistent directory * @throws IOException if this directory exists but listing was not possible for some other reason */ public abstract @Nonnull VirtualFile[] list() throws IOException; /** * Lists recursive files of this directory with pattern matching. * @param glob an Ant-style glob * @return a list of relative names of children (files directly inside or in subdirectories) * @throws IOException if this is not a directory, or listing was not possible for some other reason */ public abstract @Nonnull String[] list(String glob) throws IOException; /** * Obtains a child file. * @param name a relative path, possibly including {@code /} (but not {@code ..}) * @return a representation of that child, whether it actually exists or not */ public abstract @Nonnull VirtualFile child(@Nonnull String name); /** * Gets the file length. * @return a length, or 0 if inapplicable (e.g. a directory) * @throws IOException if checking the length failed */ public abstract long length() throws IOException; /** * Gets the file timestamp. * @return a length, or 0 if inapplicable * @throws IOException if checking the timestamp failed */ public abstract long lastModified() throws IOException; /** * Checks whether this file can be read. * @return true normally * @throws IOException if checking status failed */ public abstract boolean canRead() throws IOException; /** * Opens an input stream on the file so its contents can be read. * @return an open stream * @throws IOException if it could not be opened */ public abstract InputStream open() throws IOException; /** * Does case-insensitive comparison. * {@inheritDoc} */ @Override public final int compareTo(VirtualFile o) { return getName().compareToIgnoreCase(o.getName()); } /** * Compares according to {@link #toURI}. * {@inheritDoc} */ @Override public final boolean equals(Object obj) { return obj instanceof VirtualFile && toURI().equals(((VirtualFile) obj).toURI()); } /** * Hashes according to {@link #toURI}. * {@inheritDoc} */ @Override public final int hashCode() { return toURI().hashCode(); } /** * Displays {@link #toURI}. * {@inheritDoc} */ @Override public final String toString() { return toURI().toString(); } /** * Does some calculations in batch. * For a remote file, this can be much faster than doing the corresponding operations one by one as separate requests. * The default implementation just calls the block directly. * @param <V> a value type * @param <T> the exception type * @param callable something to run all at once (only helpful if any mentioned files are on the same system) * @return the callable result * @throws IOException if remote communication failed * @since 1.554 */ public <V> V run(Callable<V,IOException> callable) throws IOException { return callable.call(); } /** * Creates a virtual file wrapper for a local file. * @param f a disk file (need not exist) * @return a wrapper */ public static VirtualFile forFile(final File f) { return new FileVF(f, f); } private static final class FileVF extends VirtualFile { private final File f; private final File root; FileVF(File f, File root) { this.f = f; this.root = root; } @Override public String getName() { return f.getName(); } @Override public URI toURI() { return f.toURI(); } @Override public VirtualFile getParent() { return new FileVF(f.getParentFile(), root); } @Override public boolean isDirectory() throws IOException { if (isIllegalSymlink()) { return false; } return f.isDirectory(); } @Override public boolean isFile() throws IOException { if (isIllegalSymlink()) { return false; } return f.isFile(); } @Override public boolean exists() throws IOException { if (isIllegalSymlink()) { return false; } return f.exists(); } @Override public VirtualFile[] list() throws IOException { if (isIllegalSymlink()) { return new VirtualFile[0]; } File[] kids = f.listFiles(); if (kids == null) { return new VirtualFile[0]; } VirtualFile[] vfs = new VirtualFile[kids.length]; for (int i = 0; i < kids.length; i++) { vfs[i] = new FileVF(kids[i], root); } return vfs; } @Override public String[] list(String glob) throws IOException { if (isIllegalSymlink()) { return new String[0]; } return new Scanner(glob).invoke(f, null); } @Override public VirtualFile child(String name) { return new FileVF(new File(f, name), root); } @Override public long length() throws IOException { if (isIllegalSymlink()) { return 0; } return f.length(); } @Override public long lastModified() throws IOException { if (isIllegalSymlink()) { return 0; } return f.lastModified(); } @Override public boolean canRead() throws IOException { if (isIllegalSymlink()) { return false; } return f.canRead(); } @Override public InputStream open() throws IOException { if (isIllegalSymlink()) { throw new FileNotFoundException(f.getPath()); } return new FileInputStream(f); } private boolean isIllegalSymlink() { // TODO JENKINS-26838 try { String myPath = f.toPath().toRealPath(new LinkOption[0]).toString(); String rootPath = root.toPath().toRealPath(new LinkOption[0]).toString(); if (!myPath.equals(rootPath) && !myPath.startsWith(rootPath + File.separatorChar)) { return true; } } catch (IOException x) { Logger.getLogger(VirtualFile.class.getName()).log(Level.FINE, "could not determine symlink status of " + f, x); } catch (InvalidPathException x2) { // if this cannot be converted to a path, it cannot be an illegal symlink, as it cannot exist Logger.getLogger(VirtualFile.class.getName()).log(Level.FINE, "Could not convert " + f + " to path", x2); } return false; } } /** * Creates a virtual file wrapper for a remotable file. * @param f a local or remote file (need not exist) * @return a wrapper */ public static VirtualFile forFilePath(final FilePath f) { return new FilePathVF(f); } private static final class FilePathVF extends VirtualFile { private final FilePath f; FilePathVF(FilePath f) { this.f = f; } @Override public String getName() { return f.getName(); } @Override public URI toURI() { try { return f.toURI(); } catch (Exception x) { return URI.create(f.getRemote()); } } @Override public VirtualFile getParent() { return f.getParent().toVirtualFile(); } @Override public boolean isDirectory() throws IOException { try { return f.isDirectory(); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } } @Override public boolean isFile() throws IOException { // TODO should probably introduce a method for this purpose return exists() && !isDirectory(); } @Override public boolean exists() throws IOException { try { return f.exists(); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } } @Override public VirtualFile[] list() throws IOException { try { List<FilePath> kids = f.list(); if (kids == null) { return new VirtualFile[0]; } VirtualFile[] vfs = new VirtualFile[kids.size()]; for (int i = 0; i < vfs.length; i++) { vfs[i] = forFilePath(kids.get(i)); } return vfs; } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } } @Override public String[] list(String glob) throws IOException { try { return f.act(new Scanner(glob)); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } } @Override public VirtualFile child(String name) { return forFilePath(f.child(name)); } @Override public long length() throws IOException { try { return f.length(); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } } @Override public long lastModified() throws IOException { try { return f.lastModified(); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } } @Override public boolean canRead() throws IOException { try { return f.act(new Readable()); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } } @Override public InputStream open() throws IOException { try { return f.read(); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } } @Override public <V> V run(Callable<V,IOException> callable) throws IOException { try { return f.act(callable); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } } } private static final class Scanner extends MasterToSlaveFileCallable<String[]> { private final String glob; Scanner(String glob) { this.glob = glob; } @Override public String[] invoke(File f, VirtualChannel channel) throws IOException { final List<String> paths = new ArrayList<String>(); new DirScanner.Glob(glob, null).scan(f, new FileVisitor() { @Override public void visit(File f, String relativePath) throws IOException { paths.add(relativePath); } }); return paths.toArray(new String[paths.size()]); } } private static final class Readable extends MasterToSlaveFileCallable<Boolean> { @Override public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { return f.canRead(); } } }