/*******************************************************************************
*
* Copyright (c) 2004-2011 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Eric Lefevre-Ardant, Erik Ramfelt, Michael B. Donohue, Alan Harder, Manufacture Francaise des Pneumatiques Michelin, Romain Seguy, Winston Prakash, Anton Kozak
*
*
*******************************************************************************/
package hudson;
import hudson.Launcher.LocalLauncher;
import hudson.Launcher.RemoteLauncher;
import hudson.model.AbstractProject;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.TaskListener;
import hudson.remoting.Callable;
import hudson.remoting.Channel;
import hudson.remoting.DelegatingCallable;
import hudson.remoting.Future;
import hudson.remoting.Pipe;
import hudson.remoting.RemoteInputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
import hudson.util.DirScanner;
import hudson.util.FormValidation;
import hudson.util.HeadBufferingStream;
import hudson.util.IOException2;
import hudson.util.IOUtils;
import org.eclipse.hudson.jna.NativeUtils;
import hudson.org.apache.tools.tar.TarInputStream;
import org.eclipse.hudson.jna.NativeAccessException;
import hudson.util.io.Archiver;
import hudson.util.io.ArchiverFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.Writer;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipInputStream;
import java.util.logging.Logger;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.tar.TarEntry;
import org.kohsuke.stapler.Stapler;
import static hudson.FilePath.TarCompression.GZIP;
import static hudson.Util.fixEmpty;
/**
* {@link File} like object with remoting support.
*
* <p> Unlike {@link File}, which always implies a file path on the current
* computer, {@link FilePath} represents a file path on a specific slave or the
* master.
*
* Despite that, {@link FilePath} can be used much like {@link File}. It exposes
* a bunch of operations (and we should add more operations as long as they are
* generally useful), and when invoked against a file on a remote node,
* {@link FilePath} executes the necessary code remotely, thereby providing
* semi-transparent file operations.
*
* <h2>Using {@link FilePath} smartly</h2> <p> The transparency makes it easy to
* write plugins without worrying too much about remoting, by making it works
* like NFS, where remoting happens at the file-system layer.
*
* <p> But one should note that such use of remoting may not be optional.
* Sometimes, it makes more sense to move some computation closer to the data,
* as opposed to move the data to the computation. For example, if you are just
* computing a MD5 digest of a file, then it would make sense to do the digest
* on the host where the file is located, as opposed to send the whole data to
* the master and do MD5 digesting there.
*
* <p> {@link FilePath} supports this "code migration" by in the
* {@link #act(FileCallable)} method. One can pass in a custom implementation of
* {@link FileCallable}, to be executed on the node where the data is located.
* The following code shows the example:
*
* <pre>
* FilePath file = ...;
*
* // make 'file' a fresh empty directory.
* file.act(new FileCallable<Void>() {
* // if 'file' is on a different node, this FileCallable will
* // be transfered to that node and executed there.
* public Void invoke(File f,VirtualChannel channel) {
* // f and file represents the same thing
* f.deleteContents();
* f.mkdirs();
* }
* });
* </pre>
*
* <p> When {@link FileCallable} is transfered to a remote node, it will be done
* so by using the same Java serialization scheme that the remoting module uses.
* See {@link Channel} for more about this.
*
* <p> {@link FilePath} itself can be sent over to a remote node as a part of
* {@link Callable} serialization. For example, sending a {@link FilePath} of a
* remote node to that node causes {@link FilePath} to become "local".
* Similarly, sending a {@link FilePath} that represents the local computer
* causes it to become "remote."
*
* @author Kohsuke Kawaguchi
*/
public final class FilePath implements Serializable {
/**
* When this {@link FilePath} represents the remote path, this field is
* always non-null on master (the field represents the channel to the remote
* slave.) When transferred to a slave via remoting, this field reverts back
* to null, since it's transient.
*
* When this {@link FilePath} represents a path on the master, this field is
* null on master. When transferred to a slave via remoting, this field
* becomes non-null, representing the {@link Channel} back to the master.
*
* This is used to determine whether we are running on the master or the
* slave.
*/
private transient VirtualChannel channel;
// since the platform of the slave might be different, can't use java.io.File
private final String remote;
/**
* Creates a {@link FilePath} that represents a path on the given node.
*
* @param channel To create a path that represents a remote path, pass in a
* {@link Channel} that's connected to that machine. If null, that means the
* local file path.
*/
public FilePath(VirtualChannel channel, String remote) {
this.channel = channel;
this.remote = normalize(remote);
}
/**
* To create {@link FilePath} that represents a "local" path.
*
* <p> A "local" path means a file path on the computer where the
* constructor invocation happened.
*/
public FilePath(File localPath) {
this.channel = null;
this.remote = normalize(localPath.getPath());
}
/**
* Construct a path starting with a base location.
*
* @param base starting point for resolution, and defines channel
* @param rel a path which if relative will be resolved against base
*/
public FilePath(FilePath base, String rel) {
this.channel = base.channel;
if (isAbsolute(rel)) {
// absolute
this.remote = normalize(rel);
} else if (base.isUnix()) {
this.remote = normalize(base.remote + '/' + rel);
} else {
//Normalize rel path for windows environment. See http://issues.hudson-ci.org/browse/HUDSON-5084
this.remote = normalize(base.remote + '\\' + StringUtils.replace(rel, "/", "\\"));
}
}
private static boolean isAbsolute(String rel) {
return rel.startsWith("/") || DRIVE_PATTERN.matcher(rel).matches();
}
private static final Pattern DRIVE_PATTERN = Pattern.compile("[A-Za-z]:[\\\\/].*"),
ABSOLUTE_PREFIX_PATTERN = Pattern.compile("^(\\\\\\\\|(?:[A-Za-z]:)?[\\\\/])[\\\\/]*");
/**
* {@link File#getParent()} etc cannot handle ".." and "." in the path
* component very well, so remove them.
*/
private static String normalize(String path) {
StringBuilder buf = new StringBuilder();
// Check for prefix designating absolute path
Matcher m = ABSOLUTE_PREFIX_PATTERN.matcher(path);
if (m.find()) {
buf.append(m.group(1));
path = path.substring(m.end());
}
boolean isAbsolute = buf.length() > 0;
// Split remaining path into tokens, trimming any duplicate or trailing separators
List<String> tokens = new ArrayList<String>();
int s = 0, end = path.length();
for (int i = 0; i < end; i++) {
char c = path.charAt(i);
if (c == '/' || c == '\\') {
tokens.add(path.substring(s, i));
s = i;
// Skip any extra separator chars
while (++i < end && ((c = path.charAt(i)) == '/' || c == '\\')) {
}
// Add token for separator unless we reached the end
if (i < end) {
tokens.add(path.substring(s, s + 1));
}
s = i;
}
}
if (s < end) {
tokens.add(path.substring(s));
}
// Look through tokens for "." or ".."
for (int i = 0; i < tokens.size();) {
String token = tokens.get(i);
if (token.equals(".")) {
tokens.remove(i);
if (tokens.size() > 0) {
tokens.remove(i > 0 ? i - 1 : i);
}
} else if (token.equals("..")) {
if (i == 0) {
// If absolute path, just remove: /../something
// If relative path, not collapsible so leave as-is
tokens.remove(0);
if (tokens.size() > 0) {
token += tokens.remove(0);
}
if (!isAbsolute) {
buf.append(token);
}
} else {
// Normalize: remove something/.. plus separator before/after
i -= 2;
for (int j = 0; j < 3; j++) {
tokens.remove(i);
}
if (i > 0) {
tokens.remove(i - 1);
} else if (tokens.size() > 0) {
tokens.remove(0);
}
}
} else {
i += 2;
}
}
// Recombine tokens
for (String token : tokens) {
buf.append(token);
}
if (buf.length() == 0) {
buf.append('.');
}
return buf.toString();
}
/**
* Checks if the remote path is Unix.
*/
private boolean isUnix() {
// if the path represents a local path, there' no need to guess.
if (!isRemote()) {
return File.pathSeparatorChar != ';';
}
// note that we can't use the usual File.pathSeparator and etc., as the OS of
// the machine where this code runs and the OS that this FilePath refers to may be different.
// Windows absolute path is 'X:\...', so this is usually a good indication of Windows path
if (remote.length() > 3 && remote.charAt(1) == ':' && remote.charAt(2) == '\\') {
return false;
}
// Windows can handle '/' as a path separator but Unix can't,
// so err on Unix side
return remote.indexOf("\\") == -1;
}
public String getRemote() {
return remote;
}
/**
* Creates a zip file from this directory or a file and sends that to the
* given output stream.
*
* @deprecated as of 1.315. Use {@link #zip(OutputStream)} that has more
* consistent name.
*/
public void createZipArchive(OutputStream os) throws IOException, InterruptedException {
zip(os);
}
/**
* Creates a zip file from this directory or a file and sends that to the
* given output stream.
*/
public void zip(OutputStream os) throws IOException, InterruptedException {
zip(os, (FileFilter) null);
}
/**
* Creates a zip file from this directory by using the specified filter, and
* sends the result to the given output stream.
*
* @param filter Must be serializable since it may be executed remotely. Can
* be null to add all files.
*
* @since 1.315
*/
public void zip(OutputStream os, FileFilter filter) throws IOException, InterruptedException {
archive(ArchiverFactory.ZIP, os, filter);
}
/**
* Creates a zip file from this directory by only including the files that
* match the given glob.
*
* @param glob Ant style glob, like "**/*.xml". If empty or null, this
* method works like {@link #createZipArchive(OutputStream)}
*
* @since 1.129
* @deprecated as of 1.315 Use {@link #zip(OutputStream,String)} that has
* more consistent name.
*/
public void createZipArchive(OutputStream os, final String glob) throws IOException, InterruptedException {
archive(ArchiverFactory.ZIP, os, glob);
}
/**
* Creates a zip file from this directory by only including the files that
* match the given glob.
*
* @param glob Ant style glob, like "**/*.xml". If empty or null, this
* method works like {@link #createZipArchive(OutputStream)}
*
* @since 1.315
*/
public void zip(OutputStream os, final String glob) throws IOException, InterruptedException {
archive(ArchiverFactory.ZIP, os, glob);
}
/**
* Uses the given scanner on 'this' directory to list up files and then
* archive it to a zip stream.
*/
public int zip(OutputStream out, DirScanner scanner) throws IOException, InterruptedException {
return archive(ArchiverFactory.ZIP, out, scanner);
}
/**
* Archives this directory into the specified archive format, to the given
* {@link OutputStream}, by using {@link DirScanner} to choose what files to
* include.
*
* @return number of files/directories archived. This is only really useful
* to check for a situation where nothing is archived.
*/
public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner) throws IOException, InterruptedException {
if (channel != null) {
os = new RemoteOutputStream(os);
}
final OutputStream out = os;
return act(new FileCallable<Integer>() {
public Integer invoke(File f, VirtualChannel channel) throws IOException {
Archiver a = factory.create(out);
try {
scanner.scan(f, a);
} finally {
IOUtils.closeQuietly(a);
}
return a.countEntries();
}
private static final long serialVersionUID = 1L;
});
}
private int archive(final ArchiverFactory factory, OutputStream os, final FileFilter filter) throws IOException, InterruptedException {
return archive(factory, os, new DirScanner.Filter(filter));
}
private int archive(final ArchiverFactory factory, OutputStream os, final String glob) throws IOException, InterruptedException {
return archive(factory, os, new DirScanner.Glob(glob, null));
}
/**
* When this {@link FilePath} represents a zip file, extracts that zip file.
*
* @param target Target directory to expand files to. All the necessary
* directories will be created.
* @since 1.248
* @see #unzipFrom(InputStream)
*/
public void unzip(final FilePath target) throws IOException, InterruptedException {
target.act(new FileCallable<Void>() {
public Void invoke(File dir, VirtualChannel channel) throws IOException {
unzip(dir, FilePath.this.read());
return null;
}
private static final long serialVersionUID = 1L;
});
}
/**
* When this {@link FilePath} represents a tar file, extracts that tar file.
*
* @param target Target directory to expand files to. All the necessary
* directories will be created.
* @param compression Compression mode of this tar file.
* @since 1.292
* @see #untarFrom(InputStream, TarCompression)
*/
public void untar(final FilePath target, final TarCompression compression) throws IOException, InterruptedException {
final NativeUtils nativeUtils = NativeUtils.getInstance();
target.act(new FileCallable<Void>() {
public Void invoke(File dir, VirtualChannel channel) throws IOException {
readFromTar(FilePath.this.getName(), dir, compression.extract(FilePath.this.read()), nativeUtils);
return null;
}
private static final long serialVersionUID = 1L;
});
}
/**
* Reads the given InputStream as a zip file and extracts it into this
* directory.
*
* @param _in The stream will be closed by this method after it's fully
* read.
* @since 1.283
* @see #unzip(FilePath)
*/
public void unzipFrom(InputStream _in) throws IOException, InterruptedException {
final InputStream in = new RemoteInputStream(_in);
act(new FileCallable<Void>() {
public Void invoke(File dir, VirtualChannel channel) throws IOException {
unzip(dir, in);
return null;
}
private static final long serialVersionUID = 1L;
});
}
private void unzip(File dir, InputStream in) throws IOException {
dir = dir.getAbsoluteFile(); // without absolutization, getParentFile below seems to fail
ZipInputStream zip = new ZipInputStream(new BufferedInputStream(in));
java.util.zip.ZipEntry e;
try {
while ((e = zip.getNextEntry()) != null) {
File f = new File(dir, e.getName());
if (e.isDirectory()) {
f.mkdirs();
} else {
File p = f.getParentFile();
if (p != null) {
p.mkdirs();
}
IOUtils.copy(zip, f);
f.setLastModified(e.getTime());
zip.closeEntry();
}
}
} finally {
zip.close();
}
}
/**
* Absolutizes this {@link FilePath} and returns the new one.
*/
public FilePath absolutize() throws IOException, InterruptedException {
return new FilePath(channel, act(new FileCallable<String>() {
public String invoke(File f, VirtualChannel channel) throws IOException {
return f.getAbsolutePath();
}
}));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FilePath that = (FilePath) o;
if (channel != null ? !channel.equals(that.channel) : that.channel != null) {
return false;
}
return remote.equals(that.remote);
}
@Override
public int hashCode() {
return 31 * (channel != null ? channel.hashCode() : 0) + remote.hashCode();
}
/**
* Supported tar file compression methods.
*/
public enum TarCompression {
NONE {
public InputStream extract(InputStream in) {
return in;
}
public OutputStream compress(OutputStream out) {
return out;
}
},
GZIP {
public InputStream extract(InputStream _in) throws IOException {
HeadBufferingStream in = new HeadBufferingStream(_in, SIDE_BUFFER_SIZE);
try {
return new GZIPInputStream(in, BUFFER_SIZE);
} catch (IOException e) {
// various people reported "java.io.IOException: Not in GZIP format" here, so diagnose this problem better
in.fillSide();
throw new IOException2(e.getMessage() + "\nstream=" + Util.toHexString(in.getSideBuffer()), e);
}
}
public OutputStream compress(OutputStream out) throws IOException {
return new GZIPOutputStream(new BufferedOutputStream(out));
}
};
private static final int BUFFER_SIZE = 8192;
public abstract InputStream extract(InputStream in) throws IOException;
public abstract OutputStream compress(OutputStream in) throws IOException;
}
/**
* Reads the given InputStream as a tar file and extracts it into this
* directory.
*
* @param _in The stream will be closed by this method after it's fully
* read.
* @param compression The compression method in use.
* @since 1.292
*/
public void untarFrom(InputStream _in, final TarCompression compression) throws IOException, InterruptedException {
try {
final InputStream in = new RemoteInputStream(_in);
final NativeUtils nativeUtils = NativeUtils.getInstance();
act(new FileCallable<Void>() {
public Void invoke(File dir, VirtualChannel channel) throws IOException {
readFromTar("input stream", dir, compression.extract(in), nativeUtils);
return null;
}
private static final long serialVersionUID = 1L;
});
} finally {
IOUtils.closeQuietly(_in);
}
}
/**
* Given a tgz/zip file, extracts it to the given target directory, if
* necessary.
*
* <p> This method is a convenience method designed for installing a binary
* package to a location that supports upgrade and downgrade. Specifically,
*
* <ul> <li>If the target directory doesn't exist
* {@linkplain #mkdirs() it'll be created}. <li>The timestamp of the .tgz
* file is left in the installation directory upon extraction. <li>If the
* timestamp left in the directory doesn't match with the timestamp of the
* current archive file, the directory contents will be discarded and the
* archive file will be re-extracted. <li>If the connection is refused but
* the target directory already exists, it is left alone. </ul>
*
* @param archive The resource that represents the tgz/zip file. This URL
* must support the "Last-Modified" header. (Most common usage is to get
* this from {@link ClassLoader#getResource(String)})
* @param listener If non-null, a message will be printed to this listener
* once this method decides to extract an archive.
* @return true if the archive was extracted. false if the extraction was
* skipped because the target directory was considered up to date.
* @since 1.299
*/
public boolean installIfNecessaryFrom(URL archive, TaskListener listener, String message) throws IOException, InterruptedException {
try {
URLConnection con;
try {
con = ProxyConfiguration.open(archive);
con.connect();
} catch (IOException x) {
if (this.exists()) {
// Cannot connect now, so assume whatever was last unpacked is still OK.
if (listener != null) {
listener.getLogger().println("Skipping installation of " + archive + " to " + remote + ": " + x);
}
return false;
} else {
throw x;
}
}
long sourceTimestamp = con.getLastModified();
FilePath timestamp = this.child(".timestamp");
if (this.exists()) {
if (timestamp.exists() && sourceTimestamp == timestamp.lastModified()) {
return false; // already up to date
}
this.deleteContents();
} else {
this.mkdirs();
}
if (listener != null) {
listener.getLogger().println(message);
}
InputStream in = con.getInputStream();
CountingInputStream cis = new CountingInputStream(in);
try {
if (archive.toExternalForm().endsWith(".zip")) {
unzipFrom(cis);
} else {
untarFrom(cis, GZIP);
}
} catch (IOException e) {
throw new IOException2(String.format("Failed to unpack %s (%d bytes read of total %d)",
archive, cis.getByteCount(), con.getContentLength()), e);
}
timestamp.touch(sourceTimestamp);
return true;
} catch (IOException e) {
throw new IOException2("Failed to install " + archive + " to " + remote, e);
}
}
/**
* Reads the URL on the current VM, and writes all the data to this
* {@link FilePath} (this is different from resolving URL remotely.)
*
* @since 1.293
*/
public void copyFrom(URL url) throws IOException, InterruptedException {
InputStream in = url.openStream();
try {
copyFrom(in);
} finally {
in.close();
}
}
/**
* Replaces the content of this file by the data from the given
* {@link InputStream}.
*
* @since 1.293
*/
public void copyFrom(InputStream in) throws IOException, InterruptedException {
OutputStream os = write();
try {
IOUtils.copy(in, os);
} finally {
os.close();
}
}
/**
* Conveniene method to call {@link FilePath#copyTo(FilePath)}.
*
* @since 1.311
*/
public void copyFrom(FilePath src) throws IOException, InterruptedException {
src.copyTo(this);
}
/**
* Place the data from {@link FileItem} into the file location specified by
* this {@link FilePath} object.
*/
public void copyFrom(FileItem file) throws IOException, InterruptedException {
if (channel == null) {
try {
file.write(new File(remote));
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new IOException2(e);
}
} else {
InputStream i = file.getInputStream();
OutputStream o = write();
try {
IOUtils.copy(i, o);
} finally {
o.close();
i.close();
}
}
}
/**
* Code that gets executed on the machine where the {@link FilePath} is
* local. Used to act on {@link FilePath}.
*
* @see FilePath#act(FileCallable)
*/
public static interface FileCallable<T> extends Serializable {
/**
* Performs the computational task on the node where the data is
* located.
*
* <p> All the exceptions are forwarded to the caller.
*
* @param f {@link File} that represents the local file that
* {@link FilePath} has represented.
* @param channel The "back pointer" of the {@link Channel} that
* represents the communication with the node from where the code was
* sent.
*/
T invoke(File f, VirtualChannel channel) throws IOException, InterruptedException;
}
/**
* Executes some program on the machine that this {@link FilePath} exists,
* so that one can perform local file operations.
*/
public <T> T act(final FileCallable<T> callable) throws IOException, InterruptedException {
return act(callable, callable.getClass().getClassLoader());
}
private <T> T act(final FileCallable<T> callable, ClassLoader cl) throws IOException, InterruptedException {
if (channel != null) {
// run this on a remote system
try {
return channel.call(new FileCallableWrapper<T>(callable, cl));
} catch (TunneledInterruptedException e) {
throw (InterruptedException) new InterruptedException().initCause(e);
} catch (AbortException e) {
throw e; // pass through so that the caller can catch it as AbortException
} catch (IOException e) {
// wrap it into a new IOException so that we get the caller's stack trace as well.
throw new IOException2("remote file operation failed: " + remote + " at " + channel, e);
}
} else {
// the file is on the local machine.
return callable.invoke(new File(remote), Hudson.MasterComputer.localChannel);
}
}
/**
* Executes some program on the machine that this {@link FilePath} exists,
* so that one can perform local file operations.
*/
public <T> Future<T> actAsync(final FileCallable<T> callable) throws IOException, InterruptedException {
try {
return (channel != null ? channel : Hudson.MasterComputer.localChannel)
.callAsync(new FileCallableWrapper<T>(callable));
} catch (IOException e) {
// wrap it into a new IOException so that we get the caller's stack trace as well.
throw new IOException2("remote file operation failed", e);
}
}
/**
* Executes some program on the machine that this {@link FilePath} exists,
* so that one can perform local file operations.
*/
public <V, E extends Throwable> V act(Callable<V, E> callable) throws IOException, InterruptedException, E {
if (channel != null) {
// run this on a remote system
return channel.call(callable);
} else {
// the file is on the local machine
return callable.call();
}
}
/**
* Converts this file to the URI, relative to the machine on which this file
* is available.
*/
public URI toURI() throws IOException, InterruptedException {
return act(new FileCallable<URI>() {
public URI invoke(File f, VirtualChannel channel) {
return f.toURI();
}
});
}
/**
* Creates this directory.
*/
public void mkdirs() throws IOException, InterruptedException {
if (!act(new FileCallable<Boolean>() {
public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
if (f.mkdirs() || f.exists()) {
return true; // OK
}
// following Ant <mkdir> task to avoid possible race condition.
Thread.sleep(10);
return f.mkdirs() || f.exists();
}
})) {
throw new IOException("Failed to mkdirs: " + remote);
}
}
/**
* Deletes this directory, including all its contents recursively.
*/
public void deleteRecursive() throws IOException, InterruptedException {
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
Util.deleteRecursive(f);
return null;
}
});
}
/**
* Deletes all the contents of this directory, but not the directory itself
*/
public void deleteContents() throws IOException, InterruptedException {
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
Util.deleteContentsRecursive(f);
return null;
}
});
}
/**
* Gets the file name portion except the extension.
*
* For example, "foo" for "foo.txt" and "foo.tar" for "foo.tar.gz".
*/
public String getBaseName() {
String n = getName();
int idx = n.lastIndexOf('.');
if (idx < 0) {
return n;
}
return n.substring(0, idx);
}
/**
* Gets just the file name portion.
*
* This method assumes that the file name is the same between local and
* remote.
*/
public String getName() {
String r = remote;
if (r.endsWith("\\") || r.endsWith("/")) {
r = r.substring(0, r.length() - 1);
}
int len = r.length() - 1;
while (len >= 0) {
char ch = r.charAt(len);
if (ch == '\\' || ch == '/') {
break;
}
len--;
}
return r.substring(len + 1);
}
/**
* Short for {@code getParent().child(rel)}. Useful for getting other files
* in the same directory.
*/
public FilePath sibling(String rel) {
return getParent().child(rel);
}
/**
* Returns a {@link FilePath} by adding the given suffix to this path name.
*/
public FilePath withSuffix(String suffix) {
return new FilePath(channel, remote + suffix);
}
/**
* The same as {@link FilePath#FilePath(FilePath,String)} but more OO.
*
* @param rel a relative or absolute path
* @return a file on the same channel
*/
public FilePath child(String rel) {
return new FilePath(this, rel);
}
/**
* Gets the parent file.
*
* @return parent FilePath or null if there is no parent
*/
public FilePath getParent() {
int i = remote.length() - 2;
for (; i >= 0; i--) {
char ch = remote.charAt(i);
if (ch == '\\' || ch == '/') {
break;
}
}
return i >= 0 ? new FilePath(channel, remote.substring(0, i + 1)) : null;
}
/**
* Creates a temporary file in the directory that this {@link FilePath}
* object designates.
*/
public FilePath createTempFile(final String prefix, final String suffix) throws IOException, InterruptedException {
try {
return new FilePath(this, act(new FileCallable<String>() {
public String invoke(File dir, VirtualChannel channel) throws IOException {
File f = File.createTempFile(prefix, suffix, dir);
return f.getName();
}
}));
} catch (IOException e) {
throw new IOException2("Failed to create a temp file on " + remote, e);
}
}
/**
* Creates a temporary file in this directory and set the contents by the
* given text (encoded in the platform default encoding)
*/
public FilePath createTextTempFile(final String prefix, final String suffix, final String contents) throws IOException, InterruptedException {
return createTextTempFile(prefix, suffix, contents, true);
}
/**
* Creates a temporary file in this directory and set the contents by the
* given text (encoded in the platform default encoding)
*/
public FilePath createTextTempFile(final String prefix, final String suffix, final String contents, final boolean inThisDirectory) throws IOException, InterruptedException {
try {
return new FilePath(channel, act(new FileCallable<String>() {
public String invoke(File dir, VirtualChannel channel) throws IOException {
if (!inThisDirectory) {
dir = new File(System.getProperty("java.io.tmpdir"));
} else {
dir.mkdirs();
}
File f;
try {
f = File.createTempFile(prefix, suffix, dir);
} catch (IOException e) {
throw new IOException2("Failed to create a temporary directory in " + dir, e);
}
Writer w = null;
try {
w = new FileWriter(f);
w.write(contents);
} finally {
IOUtils.closeQuietly(w);
}
return f.getAbsolutePath();
}
}));
} catch (IOException e) {
throw new IOException2("Failed to create a temp file on " + remote, e);
}
}
/**
* Creates a temporary directory inside the directory represented by 'this'
*
* @since 1.311
*/
public FilePath createTempDir(final String prefix, final String suffix) throws IOException, InterruptedException {
try {
return new FilePath(this, act(new FileCallable<String>() {
public String invoke(File dir, VirtualChannel channel) throws IOException {
File f = File.createTempFile(prefix, suffix, dir);
f.delete();
f.mkdir();
return f.getName();
}
}));
} catch (IOException e) {
throw new IOException2("Failed to create a temp directory on " + remote, e);
}
}
/**
* Deletes this file.
*
* @throws IOException if it exists but could not be successfully deleted
* @return true, for a modicum of compatibility
*/
public boolean delete() throws IOException, InterruptedException {
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
Util.deleteFile(f);
return null;
}
});
return true;
}
/**
* Checks if the file exists.
*/
public boolean exists() throws IOException, InterruptedException {
return act(new FileCallable<Boolean>() {
public Boolean invoke(File f, VirtualChannel channel) throws IOException {
return f.exists();
}
});
}
/**
* Gets the last modified time stamp of this file, by using the clock of the
* machine where this file actually resides.
*
* @see File#lastModified()
* @see #touch(long)
*/
public long lastModified() throws IOException, InterruptedException {
return act(new FileCallable<Long>() {
public Long invoke(File f, VirtualChannel channel) throws IOException {
return f.lastModified();
}
});
}
/**
* Creates a file (if not already exist) and sets the timestamp.
*
* @since 1.299
*/
public void touch(final long timestamp) throws IOException, InterruptedException {
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
if (!f.exists()) {
new FileOutputStream(f).close();
}
if (!f.setLastModified(timestamp)) {
throw new IOException("Failed to set the timestamp of " + f + " to " + timestamp);
}
return null;
}
});
}
/**
* Checks if the file is a directory.
*/
public boolean isDirectory() throws IOException, InterruptedException {
return act(new FileCallable<Boolean>() {
public Boolean invoke(File f, VirtualChannel channel) throws IOException {
return f.isDirectory();
}
});
}
/**
* Returns the file size in bytes.
*
* @since 1.129
*/
public long length() throws IOException, InterruptedException {
return act(new FileCallable<Long>() {
public Long invoke(File f, VirtualChannel channel) throws IOException {
return f.length();
}
});
}
/**
* Sets the file permission.
*
* On Windows, no-op.
*
* @param mask File permission mask. To simplify the permission copying, if
* the parameter is -1, this method becomes no-op.
* @since 1.303
* @see #mode()
*/
public void chmod(final int mask) throws IOException, InterruptedException {
if (!isUnix() || mask == -1) {
return;
}
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
Util.chmod(f, mask);
return null;
}
});
}
/**
* Gets the file permission bit mask.
*
* @return -1 on Windows, since such a concept doesn't make sense.
* @since 1.311
* @see #chmod(int)
*/
public int mode() throws IOException, InterruptedException {
if (!isUnix()) {
return -1;
}
return act(new FileCallable<Integer>() {
public Integer invoke(File f, VirtualChannel channel) throws IOException {
int mode = -1;
try {
mode = NativeUtils.getInstance().mode(f);
} catch (NativeAccessException ex) {
LOGGER.log(Level.WARNING, "Native function mod failed.");
}
return mode;
}
});
}
/**
* List up files and directories in this directory.
*
* <p> This method returns direct children of the directory denoted by the
* 'this' object.
*/
public List<FilePath> list() throws IOException, InterruptedException {
return list((FileFilter) null);
}
/**
* List up subdirectories.
*
* @return can be empty but never null. Doesn't contain "." and ".."
*/
public List<FilePath> listDirectories() throws IOException, InterruptedException {
return list(new DirectoryFilter());
}
private static final class DirectoryFilter implements FileFilter, Serializable {
public boolean accept(File f) {
return f.isDirectory();
}
private static final long serialVersionUID = 1L;
}
/**
* List up files in this directory, just like
* {@link File#listFiles(FileFilter)}.
*
* @param filter The optional filter used to narrow down the result. If
* non-null, must be {@link Serializable}. If this {@link FilePath}
* represents a remote path, the filter object will be executed on the
* remote machine.
*/
public List<FilePath> list(final FileFilter filter) throws IOException, InterruptedException {
if (filter != null && !(filter instanceof Serializable)) {
throw new IllegalArgumentException("Non-serializable filter of " + filter.getClass());
}
return act(new FileCallable<List<FilePath>>() {
public List<FilePath> invoke(File f, VirtualChannel channel) throws IOException {
File[] children = f.listFiles(filter);
if (children == null) {
return null;
}
ArrayList<FilePath> r = new ArrayList<FilePath>(children.length);
for (File child : children) {
r.add(new FilePath(child));
}
return r;
}
}, (filter != null ? filter : this).getClass().getClassLoader());
}
/**
* List up files in this directory that matches the given Ant-style filter.
*
* @param includes
* See {@link FileSet} for the syntax. String like "foo/*.zip" or "foo/**/*.xml"
* @return
* can be empty but always non-null.
*/
public FilePath[] list(final String includes) throws IOException, InterruptedException {
return list(includes, null);
}
public FilePath[] list(final String includes, final String excludes) throws IOException, InterruptedException {
return act(new FileCallable<FilePath[]>() {
public FilePath[] invoke(File f, VirtualChannel channel) throws IOException {
String[] files = glob(f,includes,excludes);
FilePath[] r = new FilePath[files.length];
for( int i=0; i<r.length; i++ )
r[i] = new FilePath(new File(f,files[i]));
return r;
}
});
}
/**
* Runs Ant glob expansion.
*
* @return
* A set of relative file names from the base directory.
*/
private static String[] glob(File dir, String includes, String excludes) throws IOException {
if(isAbsolute(includes))
throw new IOException("Expecting Ant GLOB pattern, but saw '"+includes+"'. See http://ant.apache.org/manual/Types/fileset.html for syntax");
FileSet fs = Util.createFileSet(dir,includes,excludes);
DirectoryScanner ds = fs.getDirectoryScanner(new Project());
String[] files = ds.getIncludedFiles();
return files;
}
/**
* Reads this file.
*/
public InputStream read() throws IOException {
if (channel == null) {
return new FileInputStream(new File(remote));
}
final Pipe p = Pipe.createRemoteToLocal();
channel.callAsync(new Callable<Void, IOException>() {
public Void call() throws IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream(new File(remote));
Util.copyStream(fis, p.getOut());
return null;
} finally {
IOUtils.closeQuietly(fis);
IOUtils.closeQuietly(p.getOut());
}
}
});
return p.getIn();
}
/**
* Reads this file into a string, by using the current system encoding.
*/
public String readToString() throws IOException {
InputStream in = read();
try {
return IOUtils.toString(in);
} finally {
in.close();
}
}
/**
* Writes to this file. If this file already exists, it will be overwritten.
* If the directory doesn't exist, it will be created.
*/
public OutputStream write() throws IOException, InterruptedException {
if (channel == null) {
File f = new File(remote).getAbsoluteFile();
f.getParentFile().mkdirs();
return new FileOutputStream(f);
}
return channel.call(new Callable<OutputStream, IOException>() {
public OutputStream call() throws IOException {
File f = new File(remote).getAbsoluteFile();
f.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(f);
return new RemoteOutputStream(fos);
}
});
}
/**
* Overwrites this file by placing the given String as the content.
*
* @param encoding Null to use the platform default encoding.
* @since 1.105
*/
public void write(final String content, final String encoding) throws IOException, InterruptedException {
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
f.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(f);
Writer w;
if (encoding != null) {
w = new OutputStreamWriter(fos, encoding);
} else {
w = new OutputStreamWriter(fos);
}
try {
w.write(content);
} finally {
IOUtils.closeQuietly(w);
}
return null;
}
});
}
/**
* Computes the MD5 digest of the file in hex string.
*/
public String digest() throws IOException, InterruptedException {
return act(new FileCallable<String>() {
public String invoke(File f, VirtualChannel channel) throws IOException {
return Util.getDigestOf(new FileInputStream(f));
}
});
}
/**
* Rename this file/directory to the target filepath. This FilePath and the
* target must be on the some host
*/
public void renameTo(final FilePath target) throws IOException, InterruptedException {
if (this.channel != target.channel) {
throw new IOException("renameTo target must be on the same host");
}
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
f.renameTo(new File(target.remote));
return null;
}
});
}
/**
* Moves all the contents of this directory into the specified directory,
* then delete this directory itself.
*
* @since 1.308.
*/
public void moveAllChildrenTo(final FilePath target) throws IOException, InterruptedException {
if (this.channel != target.channel) {
throw new IOException("pullUpTo target must be on the same host");
}
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
File t = new File(target.getRemote());
for (File child : f.listFiles()) {
File target = new File(t, child.getName());
if (!child.renameTo(target)) {
throw new IOException("Failed to rename " + child + " to " + target);
}
}
f.delete();
return null;
}
});
}
/**
* Copies this file to the specified target.
*/
public void copyTo(FilePath target) throws IOException, InterruptedException {
try {
OutputStream out = target.write();
try {
copyTo(out);
} finally {
out.close();
}
} catch (IOException e) {
throw new IOException2("Failed to copy " + this + " to " + target, e);
}
}
/**
* Copies this file to the specified target, with file permissions intact.
*
* @since 1.311
*/
public void copyToWithPermission(FilePath target) throws IOException, InterruptedException {
copyTo(target);
// copy file permission
target.chmod(mode());
}
/**
* Sends the contents of this file into the given {@link OutputStream}.
*/
public void copyTo(OutputStream os) throws IOException, InterruptedException {
final OutputStream out = new RemoteOutputStream(os);
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream(f);
Util.copyStream(fis, out);
try {
if (Channel.current() != null) {
Channel.current().flushPipe();
}
} catch (InterruptedException ex) {
Logger.getLogger(FilePath.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
} finally {
IOUtils.closeQuietly(fis);
IOUtils.closeQuietly(out);
}
}
});
}
/**
* Remoting interface used for
* {@link FilePath#copyRecursiveTo(String, FilePath)}.
*
* TODO: this might not be the most efficient way to do the copy.
*/
interface RemoteCopier {
/**
* @param fileName relative path name to the output file. Path separator
* must be '/'.
*/
void open(String fileName) throws IOException;
void write(byte[] buf, int len) throws IOException;
void close() throws IOException;
}
/**
* Copies the contents of this directory recursively into the specified
* target directory.
*
* @since 1.312
*/
public int copyRecursiveTo(FilePath target) throws IOException, InterruptedException {
return copyRecursiveTo("**/*", target);
}
public int copyRecursiveTo(String fileMask, FilePath target) throws IOException, InterruptedException {
return copyRecursiveTo(fileMask, null, target);
}
/**
* {
*
* @see copyRecursiveTo(String fileMask, String excludes, FilePath target,
* FilePath.TarCompression remoteCompressionType)}
*/
public int copyRecursiveTo(final String fileMask, final String excludes, final FilePath target)
throws IOException, InterruptedException {
return copyRecursiveTo(fileMask, excludes, target, FilePath.TarCompression.GZIP);
}
/**
* Copies the files that match the given file mask to the specified target
* node.
*
* @param fileMask Ant GLOB pattern. String like "foo/bar/*.xml" Multiple
* patterns can be separated by ',', and whitespace can surround ',' (so
* that you can write "abc, def" and "abc,def" to mean the same thing.
* @param excludes Files to be excluded. Can be null.
* @param remoteCompressionType compression type which will be used before
* master<->slave files transfer.
* @return the number of files copied.
*/
public int copyRecursiveTo(final String fileMask, final String excludes, final FilePath target,
final FilePath.TarCompression remoteCompressionType)
throws IOException, InterruptedException {
if (this.channel == target.channel) {
// local to local copy.
return act(new FileCallable<Integer>() {
public Integer invoke(File base, VirtualChannel channel) throws IOException {
if (!base.exists()) {
return 0;
}
assert target.channel == null;
try {
class CopyImpl extends Copy {
private int copySize;
public CopyImpl() {
setProject(new org.apache.tools.ant.Project());
}
@Override
protected void doFileOperations() {
copySize = super.fileCopyMap.size();
super.doFileOperations();
}
public int getNumCopied() {
return copySize;
}
}
CopyImpl copyTask = new CopyImpl();
copyTask.setTodir(new File(target.remote));
copyTask.addFileset(Util.createFileSet(base, fileMask, excludes));
copyTask.setOverwrite(true);
copyTask.setIncludeEmptyDirs(false);
copyTask.execute();
return copyTask.getNumCopied();
} catch (BuildException e) {
throw new IOException2("Failed to copy " + base + "/" + fileMask + " to " + target, e);
}
}
});
} else if (this.channel == null) {
// local -> remote copy
final Pipe pipe = Pipe.createLocalToRemote();
final NativeUtils nativeUtils = NativeUtils.getInstance();
Future<Void> future = target.actAsync(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
try {
readFromTar(remote + '/' + fileMask, f, (remoteCompressionType != null
? remoteCompressionType.extract(pipe.getIn())
: FilePath.TarCompression.GZIP.extract(pipe.getIn())), nativeUtils);
return null;
} finally {
pipe.getIn().close();
}
}
});
int r = writeToTar(new File(remote), fileMask, excludes, (remoteCompressionType != null
? remoteCompressionType.compress(pipe.getOut()) : FilePath.TarCompression.GZIP.compress(pipe.getOut())));
try {
future.get();
} catch (ExecutionException e) {
throw new IOException2(e);
}
return r;
} else {
// remote -> local copy
final Pipe pipe = Pipe.createRemoteToLocal();
Future<Integer> future = actAsync(new FileCallable<Integer>() {
public Integer invoke(File f, VirtualChannel channel) throws IOException {
try {
return writeToTar(f, fileMask, excludes, (remoteCompressionType != null
? remoteCompressionType.compress(pipe.getOut())
: FilePath.TarCompression.GZIP.compress(pipe.getOut())));
} finally {
pipe.getOut().close();
}
}
});
try {
final NativeUtils nativeUtils = NativeUtils.getInstance();
//it's possible to get NPE if on slave works old process
readFromTar(remote + '/' + fileMask, new File(target.remote),
(remoteCompressionType != null ? remoteCompressionType.extract(pipe.getIn())
: FilePath.TarCompression.GZIP.extract(pipe.getIn())), nativeUtils);
} catch (IOException e) { // BuildException or IOException
try {
future.get(3, TimeUnit.SECONDS);
throw e; // the remote side completed successfully, so the error must be local
} catch (ExecutionException x) {
// report both errors
throw new IOException2(Functions.printThrowable(e), x);
} catch (TimeoutException _) {
// remote is hanging
throw e;
}
}
try {
return future.get();
} catch (ExecutionException e) {
throw new IOException2(e);
}
}
}
/**
* Writes files in 'this' directory to a tar stream.
*
* @param glob Ant file pattern mask, like "**/*.java".
*/
public int tar(OutputStream out, final String glob) throws IOException, InterruptedException {
return archive(ArchiverFactory.TAR, out, glob);
}
public int tar(OutputStream out, FileFilter filter) throws IOException, InterruptedException {
return archive(ArchiverFactory.TAR, out, filter);
}
/**
* Uses the given scanner on 'this' directory to list up files and then
* archive it to a tar stream.
*/
public int tar(OutputStream out, DirScanner scanner) throws IOException, InterruptedException {
return archive(ArchiverFactory.TAR, out, scanner);
}
/**
* Writes to a tar stream and stores obtained files to the base dir.
*
* @return number of files/directories that are written.
*/
private static Integer writeToTar(File baseDir, String fileMask, String excludes, OutputStream out) throws IOException {
Archiver tw = ArchiverFactory.TAR.create(out);
try {
new DirScanner.Glob(fileMask, excludes).scan(baseDir, tw);
} finally {
tw.close();
}
return tw.countEntries();
}
/**
* Reads from a tar stream and stores obtained files to the base dir.
*/
private static void readFromTar(String name, File baseDir, InputStream in, NativeUtils nativeUtils) throws IOException {
TarInputStream t = new TarInputStream(in);
try {
TarEntry te;
while ((te = t.getNextEntry()) != null) {
File f = new File(baseDir, te.getName());
if (te.isDirectory()) {
f.mkdirs();
} else {
File parent = f.getParentFile();
if (parent != null) {
parent.mkdirs();
}
IOUtils.copy(t, f);
f.setLastModified(te.getModTime().getTime());
int mode = te.getMode() & 0777;
// be defensive
if (mode != 0 && !Functions.isWindows()) {
Util.chmod(f, mode, true, nativeUtils);
}
}
}
} catch (IOException e) {
throw new IOException2("Failed to extract " + name, e);
} finally {
t.close();
}
}
/**
* Creates a {@link Launcher} for starting processes on the node that has
* this file.
*
* @since 1.89
*/
public Launcher createLauncher(TaskListener listener) throws IOException, InterruptedException {
if (channel == null) {
return new LocalLauncher(listener);
} else {
return new RemoteLauncher(listener, channel, channel.call(new IsUnix()));
}
}
private static final class IsUnix implements Callable<Boolean, IOException> {
public Boolean call() throws IOException {
return File.pathSeparatorChar == ':';
}
private static final long serialVersionUID = 1L;
}
/**
* Validates the ant file mask (like "foo/bar/*.txt, zot/*.jar") against
* this directory, and try to point out the problem.
*
* <p> This is useful in conjunction with {@link FormValidation}.
*
* @return null if no error was found. Otherwise returns a human readable
* error message.
* @since 1.90
* @see #validateFileMask(FilePath, String)
*/
public String validateAntFileMask(final String fileMasks) throws IOException, InterruptedException {
return act(new FileCallable<String>() {
public String invoke(File dir, VirtualChannel channel) throws IOException {
if (fileMasks.startsWith("~")) {
return Messages.FilePath_TildaDoesntWork();
}
StringTokenizer tokens = new StringTokenizer(fileMasks, ",");
while (tokens.hasMoreTokens()) {
final String fileMask = tokens.nextToken().trim();
if (hasMatch(dir, fileMask)) {
continue; // no error on this portion
}
// in 1.172 we introduced an incompatible change to stop using ' ' as the separator
// so see if we can match by using ' ' as the separator
if (fileMask.contains(" ")) {
boolean matched = true;
for (String token : Util.tokenize(fileMask)) {
matched &= hasMatch(dir, token);
}
if (matched) {
return Messages.FilePath_validateAntFileMask_whitespaceSeprator();
}
}
// a common mistake is to assume the wrong base dir, and there are two variations
// to this: (1) the user gave us aa/bb/cc/dd where cc/dd was correct
// and (2) the user gave us cc/dd where aa/bb/cc/dd was correct.
{ // check the (1) above first
String f = fileMask;
while (true) {
int idx = findSeparator(f);
if (idx == -1) {
break;
}
f = f.substring(idx + 1);
if (hasMatch(dir, f)) {
return Messages.FilePath_validateAntFileMask_doesntMatchAndSuggest(fileMask, f);
}
}
}
{ // check the (2) above next as this is more expensive.
// Try prepending "**/" to see if that results in a match
FileSet fs = Util.createFileSet(dir, "**/" + fileMask);
DirectoryScanner ds = fs.getDirectoryScanner(new Project());
if (ds.getIncludedFilesCount() != 0) {
// try shorter name first so that the suggestion results in least amount of changes
String[] names = ds.getIncludedFiles();
Arrays.sort(names, SHORTER_STRING_FIRST);
for (String f : names) {
// now we want to decompose f to the leading portion that matched "**"
// and the trailing portion that matched the file mask, so that
// we can suggest the user error.
//
// this is not a very efficient/clever way to do it, but it's relatively simple
String prefix = "";
while (true) {
int idx = findSeparator(f);
if (idx == -1) {
break;
}
prefix += f.substring(0, idx) + '/';
f = f.substring(idx + 1);
if (hasMatch(dir, prefix + fileMask)) {
return Messages.FilePath_validateAntFileMask_doesntMatchAndSuggest(fileMask, prefix + fileMask);
}
}
}
}
}
{ // finally, see if we can identify any sub portion that's valid. Otherwise bail out
String previous = null;
String pattern = fileMask;
while (true) {
if (hasMatch(dir, pattern)) {
// found a match
if (previous == null) {
return Messages.FilePath_validateAntFileMask_portionMatchAndSuggest(fileMask, pattern);
} else {
return Messages.FilePath_validateAntFileMask_portionMatchButPreviousNotMatchAndSuggest(fileMask, pattern, previous);
}
}
int idx = findSeparator(pattern);
if (idx < 0) { // no more path component left to go back
if (pattern.equals(fileMask)) {
return Messages.FilePath_validateAntFileMask_doesntMatchAnything(fileMask);
} else {
return Messages.FilePath_validateAntFileMask_doesntMatchAnythingAndSuggest(fileMask, pattern);
}
}
// cut off the trailing component and try again
previous = pattern;
pattern = pattern.substring(0, idx);
}
}
}
return null; // no error
}
private boolean hasMatch(File dir, String pattern) {
FileSet fs = Util.createFileSet(dir, pattern);
DirectoryScanner ds = fs.getDirectoryScanner(new Project());
return ds.getIncludedFilesCount() != 0 || ds.getIncludedDirsCount() != 0;
}
/**
* Finds the position of the first path separator.
*/
private int findSeparator(String pattern) {
int idx1 = pattern.indexOf('\\');
int idx2 = pattern.indexOf('/');
if (idx1 == -1) {
return idx2;
}
if (idx2 == -1) {
return idx1;
}
return Math.min(idx1, idx2);
}
});
}
/**
* Shortcut for {@link #validateFileMask(String)} in case the left-hand side
* can be null.
*/
public static FormValidation validateFileMask(FilePath pathOrNull, String value) throws IOException {
if (pathOrNull == null) {
return FormValidation.ok();
}
return pathOrNull.validateFileMask(value);
}
/**
* Short for {@code validateFileMask(value,true)}
*/
public FormValidation validateFileMask(String value) throws IOException {
return validateFileMask(value, true);
}
/**
* Checks the GLOB-style file mask. See
* {@link #validateAntFileMask(String)}. Requires configure permission on
* ancestor AbstractProject object in request.
*
* @since 1.294
*/
public FormValidation validateFileMask(String value, boolean errorIfNotExist) throws IOException {
AbstractProject subject = Stapler.getCurrentRequest().findAncestorObject(AbstractProject.class);
subject.checkPermission(Item.CONFIGURE);
value = fixEmpty(value);
if (value == null) {
return FormValidation.ok();
}
try {
// no workspace. can't check
if (!exists()) {
return FormValidation.ok();
}
String msg = validateAntFileMask(value);
if (errorIfNotExist) {
return FormValidation.error(msg);
} else {
return FormValidation.warning(msg);
}
} catch (InterruptedException e) {
return FormValidation.ok();
}
}
/**
* Validates a relative file path from this {@link FilePath}. Requires
* configure permission on ancestor AbstractProject object in request.
*
* @param value The relative path being validated.
* @param errorIfNotExist If true, report an error if the given relative
* path doesn't exist. Otherwise it's a warning.
* @param expectingFile If true, we expect the relative path to point to a
* file. Otherwise, the relative path is expected to be pointing to a
* directory.
*/
public FormValidation validateRelativePath(String value, boolean errorIfNotExist, boolean expectingFile) throws IOException {
AbstractProject subject = Stapler.getCurrentRequest().findAncestorObject(AbstractProject.class);
value = fixEmpty(value);
// none entered yet, or something is seriously wrong
if (value == null || (AbstractProject<?, ?>) subject == null) {
return FormValidation.ok();
}
subject.checkPermission(Item.CONFIGURE);
// a common mistake is to use wildcard
if (value.contains("*")) {
return FormValidation.error(Messages.FilePath_validateRelativePath_wildcardNotAllowed());
}
try {
// no base directory. can't check
if (!exists()) {
return FormValidation.ok();
}
FilePath path = child(value);
if (path.exists()) {
if (expectingFile) {
if (!path.isDirectory()) {
return FormValidation.ok();
} else {
return FormValidation.error(Messages.FilePath_validateRelativePath_notFile(value));
}
} else {
if (path.isDirectory()) {
return FormValidation.ok();
} else {
return FormValidation.error(Messages.FilePath_validateRelativePath_notDirectory(value));
}
}
}
String msg = expectingFile ? Messages.FilePath_validateRelativePath_noSuchFile(value)
: Messages.FilePath_validateRelativePath_noSuchDirectory(value);
if (errorIfNotExist) {
return FormValidation.error(msg);
} else {
return FormValidation.warning(msg);
}
} catch (InterruptedException e) {
return FormValidation.ok();
}
}
/**
* A convenience method over
* {@link #validateRelativePath(String, boolean, boolean)}.
*/
public FormValidation validateRelativeDirectory(String value, boolean errorIfNotExist) throws IOException {
return validateRelativePath(value, errorIfNotExist, false);
}
public FormValidation validateRelativeDirectory(String value) throws IOException {
return validateRelativeDirectory(value, true);
}
@Deprecated
@Override
public String toString() {
// to make writing JSPs easily, return local
return remote;
}
public VirtualChannel getChannel() {
if (channel != null) {
return channel;
} else {
return Hudson.MasterComputer.localChannel;
}
}
/**
* Returns true if this {@link FilePath} represents a remote file.
*/
public boolean isRemote() {
return channel != null;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
Channel target = Channel.current();
if (channel != null && channel != target) {
throw new IllegalStateException("Can't send a remote FilePath to a different remote channel");
}
oos.defaultWriteObject();
oos.writeBoolean(channel == null);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
Channel channel = Channel.current();
assert channel != null;
ois.defaultReadObject();
if (ois.readBoolean()) {
this.channel = channel;
} else {
this.channel = null;
}
}
private static final long serialVersionUID = 1L;
public static int SIDE_BUFFER_SIZE = 1024;
private static final Logger LOGGER = Logger.getLogger(FilePath.class.getName());
/**
* Adapts {@link FileCallable} to {@link Callable}.
*/
private class FileCallableWrapper<T> implements DelegatingCallable<T, IOException> {
private final FileCallable<T> callable;
private transient ClassLoader classLoader;
public FileCallableWrapper(FileCallable<T> callable) {
this.callable = callable;
this.classLoader = callable.getClass().getClassLoader();
}
private FileCallableWrapper(FileCallable<T> callable, ClassLoader classLoader) {
this.callable = callable;
this.classLoader = classLoader;
}
public T call() throws IOException {
try {
return callable.invoke(new File(remote), Channel.current());
} catch (InterruptedException e) {
throw new TunneledInterruptedException(e);
}
}
public ClassLoader getClassLoader() {
return classLoader;
}
private static final long serialVersionUID = 1L;
}
/**
* Used to tunnel {@link InterruptedException} over a Java signature that
* only allows {@link IOException}
*/
private static class TunneledInterruptedException extends IOException2 {
private TunneledInterruptedException(InterruptedException cause) {
super(cause);
}
private static final long serialVersionUID = 1L;
}
private static final Comparator<String> SHORTER_STRING_FIRST = new Comparator<String>() {
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
};
}