提交 b698b672 编写于 作者: K Kohsuke Kawaguchi

[FIXED JENKINS-17236] Introduce ArtifactManager

Merge branch 'ArtifactManager-JENKINS-17236'
......@@ -67,6 +67,9 @@ Upcoming changes</a>
<li class=rfe>
Display the full display name in title for jobs and views.
(<a href="https://github.com/jenkinsci/jenkins/pull/884">pull request 884</a>)
<li class='major rfe'>
Added a new extension point to control where archived artifacts get stored.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-17236">issue 17236</a>)
</ul>
</div><!--=TRUNK-END=-->
......
......@@ -53,10 +53,9 @@ import static hudson.FilePath.TarCompression.GZIP;
import hudson.org.apache.tools.tar.TarInputStream;
import hudson.util.io.Archiver;
import hudson.util.io.ArchiverFactory;
import org.apache.tools.ant.BuildException;
import jenkins.util.VirtualFile;
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.apache.commons.io.input.CountingInputStream;
......@@ -98,8 +97,10 @@ import com.jcraft.jzlib.GZIPOutputStream;
import com.sun.jna.Native;
import hudson.os.PosixException;
import java.io.BufferedInputStream;
import hudson.util.FileVisitor;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import org.apache.tools.ant.taskdefs.Chmod;
......@@ -197,7 +198,7 @@ public final class FilePath implements Serializable {
* that's connected to that machine. If null, that means the local file path.
*/
public FilePath(VirtualChannel channel, String remote) {
this.channel = channel;
this.channel = channel == Jenkins.MasterComputer.localChannel ? null : channel;
this.remote = normalize(remote);
}
......@@ -1041,6 +1042,13 @@ public final class FilePath implements Serializable {
});
}
/**
* Gets the {@link VirtualFile} representation of this {@link FilePath}
*/
public VirtualFile toVirtualFile() {
return VirtualFile.forFilePath(this);
}
/**
* Creates this directory.
*/
......@@ -1880,6 +1888,18 @@ public final class FilePath implements Serializable {
* the number of files copied.
*/
public int copyRecursiveTo(final String fileMask, final String excludes, final FilePath target) throws IOException, InterruptedException {
return copyRecursiveTo(new DirScanner.Glob(fileMask, excludes), target, fileMask);
}
/**
* Copies files according to a specified scanner to a target node.
* @param scanner a way of enumerating some files (must be serializable for possible delivery to remote side)
* @param target the destination basedir
* @param description a description of the fileset, for logging purposes
* @return the number of files copied
* @since 1.531
*/
public int copyRecursiveTo(final DirScanner scanner, final FilePath target, final String description) throws IOException, InterruptedException {
if(this.channel==target.channel) {
// local to local copy.
return act(new FileCallable<Integer>() {
......@@ -1887,37 +1907,30 @@ public final class FilePath implements Serializable {
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();
final File dest = new File(target.remote);
final AtomicInteger count = new AtomicInteger();
scanner.scan(base, new FileVisitor() {
@Override public void visit(File f, String relativePath) throws IOException {
if (f.isFile()) {
File target = new File(dest, relativePath);
target.getParentFile().mkdirs();
Util.copyFile(f, target);
count.incrementAndGet();
}
public int getNumCopied() {
return copySize;
}
@Override public boolean understandsSymlink() {
return true;
}
@Override public void visitSymlink(File link, String target, String relativePath) throws IOException {
try {
Util.createSymlink(dest, target, relativePath, TaskListener.NULL);
} catch (InterruptedException x) {
throw (IOException) new IOException(x.toString()).initCause(x);
}
count.incrementAndGet();
}
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);
}
});
return count.get();
}
});
} else
......@@ -1929,7 +1942,7 @@ public final class FilePath implements Serializable {
private static final long serialVersionUID = 1L;
public Void invoke(File f, VirtualChannel channel) throws IOException {
try {
readFromTar(remote+'/'+fileMask, f,TarCompression.GZIP.extract(pipe.getIn()));
readFromTar(remote + '/' + description, f,TarCompression.GZIP.extract(pipe.getIn()));
return null;
} finally {
pipe.getIn().close();
......@@ -1939,7 +1952,7 @@ public final class FilePath implements Serializable {
Future<Integer> future2 = actAsync(new FileCallable<Integer>() {
private static final long serialVersionUID = 1L;
@Override public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
return writeToTar(new File(remote),fileMask,excludes,TarCompression.GZIP.compress(pipe.getOut()));
return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut()));
}
});
try {
......@@ -1957,14 +1970,14 @@ public final class FilePath implements Serializable {
private static final long serialVersionUID = 1L;
public Integer invoke(File f, VirtualChannel channel) throws IOException {
try {
return writeToTar(f,fileMask,excludes,TarCompression.GZIP.compress(pipe.getOut()));
return writeToTar(f, scanner, TarCompression.GZIP.compress(pipe.getOut()));
} finally {
pipe.getOut().close();
}
}
});
try {
readFromTar(remote+'/'+fileMask,new File(target.remote),TarCompression.GZIP.extract(pipe.getIn()));
readFromTar(remote + '/' + description,new File(target.remote),TarCompression.GZIP.extract(pipe.getIn()));
} catch (IOException e) {// BuildException or IOException
try {
future.get(3,TimeUnit.SECONDS);
......@@ -2013,10 +2026,10 @@ public final class FilePath implements Serializable {
* @return
* number of files/directories that are written.
*/
private static Integer writeToTar(File baseDir, String fileMask, String excludes, OutputStream out) throws IOException {
private static Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out) throws IOException {
Archiver tw = ArchiverFactory.TAR.create(out);
try {
new DirScanner.Glob(fileMask,excludes).scan(baseDir,tw);
scanner.scan(baseDir,tw);
} finally {
tw.close();
}
......@@ -2479,4 +2492,35 @@ public final class FilePath implements Serializable {
}
});
}
/**
* Helper class to make it easy to send an explicit list of files using {@link FilePath} methods.
* @since 1.531
*/
public static final class ExplicitlySpecifiedDirScanner extends DirScanner {
private static final long serialVersionUID = 1;
private final Map<String,String> files;
/**
* Create a “scanner” (it actually does no scanning).
* @param files a map from logical relative paths as per {@link FileVisitor#visit}, to actual relative paths within the scanned directory
*/
public ExplicitlySpecifiedDirScanner(Map<String,String> files) {
this.files = files;
}
@Override public void scan(File dir, FileVisitor visitor) throws IOException {
for (Map.Entry<String,String> entry : files.entrySet()) {
String archivedPath = entry.getKey();
assert archivedPath.indexOf('\\') == -1;
String workspacePath = entry.getValue();
assert workspacePath.indexOf('\\') == -1;
scanSingle(new File(dir, workspacePath), archivedPath, visitor);
}
}
}
}
......@@ -26,19 +26,13 @@ package hudson.model;
import hudson.FilePath;
import hudson.Util;
import hudson.util.IOException2;
import hudson.FilePath.FileCallable;
import hudson.remoting.VirtualChannel;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.HttpResponse;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.DirectoryScanner;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
......@@ -53,6 +47,9 @@ import java.util.Locale;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import jenkins.util.VirtualFile;
/**
* Has convenience methods to serve file system.
......@@ -69,7 +66,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
public final String title;
private final FilePath base;
private final VirtualFile base;
private final String icon;
private final boolean serveDirIndex;
private String indexFileName = "index.html";
......@@ -79,7 +76,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
* Use {@link #DirectoryBrowserSupport(ModelObject, FilePath, String, String, boolean)}
*/
public DirectoryBrowserSupport(ModelObject owner, String title) {
this(owner,null,title,null,false);
this(owner, (VirtualFile) null, title, null, false);
}
/**
......@@ -96,6 +93,24 @@ public final class DirectoryBrowserSupport implements HttpResponse {
* False to serve "index.html"
*/
public DirectoryBrowserSupport(ModelObject owner, FilePath base, String title, String icon, boolean serveDirIndex) {
this(owner, base.toVirtualFile(), title, icon, serveDirIndex);
}
/**
* @param owner
* The parent model object under which the directory browsing is added.
* @param base
* The root of the directory that's bound to URL.
* @param title
* Used in the HTML caption.
* @param icon
* The icon file name, like "folder.gif"
* @param serveDirIndex
* True to generate the directory index.
* False to serve "index.html"
* @since 1.531
*/
public DirectoryBrowserSupport(ModelObject owner, VirtualFile base, String title, String icon, boolean serveDirIndex) {
this.owner = owner;
this.base = base;
this.title = title;
......@@ -133,6 +148,10 @@ public final class DirectoryBrowserSupport implements HttpResponse {
* from the {@code doXYZ} method and let Stapler generate a response for you.
*/
public void serveFile(StaplerRequest req, StaplerResponse rsp, FilePath root, String icon, boolean serveDirIndex) throws IOException, ServletException, InterruptedException {
serveFile(req, rsp, root.toVirtualFile(), icon, serveDirIndex);
}
private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root, String icon, boolean serveDirIndex) throws IOException, ServletException, InterruptedException {
// handle form submission
String pattern = req.getParameter("pattern");
if(pattern==null)
......@@ -163,7 +182,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
String pathElement = pathTokens.nextToken();
// Treat * and ? as wildcard unless they match a literal filename
if((pathElement.contains("?") || pathElement.contains("*"))
&& inBase && !(new FilePath(root, (_base.length() > 0 ? _base + "/" : "") + pathElement).exists()))
&& inBase && !root.child((_base.length() > 0 ? _base + "/" : "") + pathElement).exists())
inBase = false;
if(pathElement.equals("*zip*")) {
// the expected syntax is foo/bar/*zip*/bar.zip
......@@ -189,20 +208,23 @@ public final class DirectoryBrowserSupport implements HttpResponse {
String rest = _rest.toString();
// this is the base file/directory
FilePath baseFile = new FilePath(root,base);
VirtualFile baseFile = root.child(base);
if(baseFile.isDirectory()) {
if(zip) {
rsp.setContentType("application/zip");
baseFile.zip(rsp.getOutputStream(),rest);
zip(rsp.getOutputStream(), baseFile, rest);
return;
}
if (plain) {
rsp.setContentType("text/plain;charset=UTF-8");
OutputStream os = rsp.getOutputStream();
try {
for (String kid : baseFile.act(new SimpleChildList())) {
os.write(kid.getBytes("UTF-8"));
for (VirtualFile kid : baseFile.list()) {
os.write(kid.getName().getBytes("UTF-8"));
if (kid.isDirectory()) {
os.write('/');
}
os.write('\n');
}
os.flush();
......@@ -221,15 +243,15 @@ public final class DirectoryBrowserSupport implements HttpResponse {
}
}
FileCallable<List<List<Path>>> glob = null;
List<List<Path>> glob = null;
if(rest.length()>0) {
// the rest is Ant glob pattern
glob = new PatternScanner(rest,createBackRef(restSize));
glob = patternScan(baseFile, rest, createBackRef(restSize));
} else
if(serveDirIndex) {
// serve directory index
glob = new ChildPathBuilder(req.getLocale());
glob = buildChildPaths(baseFile, req.getLocale());
}
if(glob!=null) {
......@@ -239,7 +261,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
req.setAttribute("parentPath",parentPaths);
req.setAttribute("backPath", createBackRef(restSize));
req.setAttribute("topPath", createBackRef(parentPaths.size()+restSize));
req.setAttribute("files", baseFile.act(glob));
req.setAttribute("files", glob);
req.setAttribute("icon", icon);
req.setAttribute("path", path);
req.setAttribute("pattern",rest);
......@@ -262,24 +284,25 @@ public final class DirectoryBrowserSupport implements HttpResponse {
boolean view = rest.equals("*view*");
if(rest.equals("*fingerprint*")) {
rsp.forward(Jenkins.getInstance().getFingerprint(baseFile.digest()),"/",req);
rsp.forward(Jenkins.getInstance().getFingerprint(Util.getDigestOf(baseFile.open())), "/", req);
return;
}
ContentInfo ci = baseFile.act(new ContentInfo());
long lastModified = baseFile.lastModified();
long length = baseFile.length();
if(LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Serving "+baseFile+" with lastModified="+ci.lastModified+", contentLength="+ci.contentLength);
LOGGER.fine("Serving "+baseFile+" with lastModified=" + lastModified + ", length=" + length);
InputStream in = baseFile.read();
InputStream in = baseFile.open();
if (view) {
// for binary files, provide the file name for download
rsp.setHeader("Content-Disposition", "inline; filename=" + baseFile.getName());
// pseudo file name to let the Stapler set text/plain
rsp.serveFile(req, in, ci.lastModified, -1, ci.contentLength, "plain.txt");
rsp.serveFile(req, in, lastModified, -1, length, "plain.txt");
} else {
rsp.serveFile(req, in, ci.lastModified, -1, ci.contentLength, baseFile.getName() );
rsp.serveFile(req, in, lastModified, -1, length, baseFile.getName() );
}
}
......@@ -290,19 +313,6 @@ public final class DirectoryBrowserSupport implements HttpResponse {
return path;
}
private static final class ContentInfo implements FileCallable<ContentInfo> {
long contentLength;
long lastModified;
public ContentInfo invoke(File f, VirtualChannel channel) throws IOException {
contentLength = f.length();
lastModified = f.lastModified();
return this;
}
private static final long serialVersionUID = 1L;
}
/**
* Builds a list of {@link Path} that represents ancestors
* from a string like "/foo/bar/zot".
......@@ -328,6 +338,19 @@ public final class DirectoryBrowserSupport implements HttpResponse {
return buf.toString();
}
private static void zip(OutputStream outputStream, VirtualFile dir, String glob) throws IOException {
ZipOutputStream zos = new ZipOutputStream(outputStream);
for (String n : dir.list(glob.length() == 0 ? "**" : glob)) {
ZipEntry e = new ZipEntry(n);
VirtualFile f = dir.child(n);
e.setTime(f.lastModified());
zos.putNextEntry(e);
Util.copyStream(f.open(), outputStream);
zos.closeEntry();
}
zos.close();
}
/**
* Represents information about one file or folder.
*/
......@@ -349,7 +372,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
private final long size;
/**
* If the current user can read the file.
* If the current user can read the file.
*/
private final boolean isReadable;
......@@ -393,14 +416,14 @@ public final class DirectoryBrowserSupport implements HttpResponse {
private static final class FileComparator implements Comparator<File> {
private static final class FileComparator implements Comparator<VirtualFile> {
private Collator collator;
public FileComparator(Locale locale) {
this.collator = Collator.getInstance(locale);
}
public int compare(File lhs, File rhs) {
public int compare(VirtualFile lhs, VirtualFile rhs) {
// directories first, files next
int r = dirRank(lhs)-dirRank(rhs);
if(r!=0) return r;
......@@ -408,29 +431,13 @@ public final class DirectoryBrowserSupport implements HttpResponse {
return this.collator.compare(lhs.getName(), rhs.getName());
}
private int dirRank(File f) {
private int dirRank(VirtualFile f) {
try {
if(f.isDirectory()) return 0;
else return 1;
}
}
/**
* Simple list of names of children of a folder.
* Subfolders will have a trailing slash appended.
*/
private static final class SimpleChildList implements FileCallable<List<String>> {
private static final long serialVersionUID = 1L;
public List<String> invoke(File f, VirtualChannel channel) throws IOException {
List<String> r = new ArrayList<String>();
String[] kids = f.list(); // no need to sort
for (String kid : kids) {
if (new File(f, kid).isDirectory()) {
r.add(kid + "/");
} else {
r.add(kid);
}
} catch (IOException ex) {
return 0;
}
return r;
}
}
......@@ -439,22 +446,14 @@ public final class DirectoryBrowserSupport implements HttpResponse {
* list of {@link Path} represents one child item to be shown
* (this mechanism is used to skip empty intermediate directory.)
*/
private static final class ChildPathBuilder implements FileCallable<List<List<Path>>> {
private Locale locale;
public ChildPathBuilder(Locale locale) {
this.locale = locale;
}
public List<List<Path>> invoke(File cur, VirtualChannel channel) throws IOException {
private static List<List<Path>> buildChildPaths(VirtualFile cur, Locale locale) throws IOException {
List<List<Path>> r = new ArrayList<List<Path>>();
File[] files = cur.listFiles();
if (files != null) {
Arrays.sort(files,new FileComparator(this.locale));
VirtualFile[] files = cur.list();
Arrays.sort(files,new FileComparator(locale));
for( File f : files ) {
Path p = new Path(Util.rawEncode(f.getName()),f.getName(),f.isDirectory(),f.length(), f.canRead());
for( VirtualFile f : files ) {
Path p = new Path(Util.rawEncode(f.getName()), f.getName(), f.isDirectory(), f.length(), f.canRead());
if(!f.isDirectory()) {
r.add(Collections.singletonList(p));
} else {
......@@ -464,53 +463,38 @@ public final class DirectoryBrowserSupport implements HttpResponse {
String relPath = Util.rawEncode(f.getName());
while(true) {
// files that don't start with '.' qualify for 'meaningful files', nor SCM related files
File[] sub = f.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return !name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn");
List<VirtualFile> sub = new ArrayList<VirtualFile>();
for (VirtualFile vf : f.list()) {
String name = vf.getName();
if (!name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn")) {
sub.add(vf);
}
});
if(sub==null || sub.length!=1 || !sub[0].isDirectory())
}
if (sub.size() !=1 || !sub.get(0).isDirectory())
break;
f = sub[0];
f = sub.get(0);
relPath += '/'+Util.rawEncode(f.getName());
l.add(new Path(relPath,f.getName(),true,0, f.canRead()));
}
r.add(l);
}
}
}
return r;
}
private static final long serialVersionUID = 1L;
}
/**
* Runs ant GLOB against the current {@link FilePath} and returns matching
* paths.
* @param baseRef String like "../../../" that cancels the 'rest' portion. Can be "./"
*/
private static class PatternScanner implements FileCallable<List<List<Path>>> {
private final String pattern;
/**
* String like "../../../" that cancels the 'rest' portion. Can be "./"
*/
private final String baseRef;
public PatternScanner(String pattern,String baseRef) {
this.pattern = pattern;
this.baseRef = baseRef;
}
public List<List<Path>> invoke(File baseDir, VirtualChannel channel) throws IOException {
FileSet fs = Util.createFileSet(baseDir,pattern);
DirectoryScanner ds = fs.getDirectoryScanner();
String[] files = ds.getIncludedFiles();
private static List<List<Path>> patternScan(VirtualFile baseDir, String pattern, String baseRef) throws IOException {
String[] files = baseDir.list(pattern);
if (files.length > 0) {
List<List<Path>> r = new ArrayList<List<Path>>(files.length);
for (String match : files) {
List<Path> file = buildPathList(baseDir, new File(baseDir,match));
List<Path> file = buildPathList(baseDir, baseDir.child(match), baseRef);
r.add(file);
}
return r;
......@@ -522,7 +506,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
/**
* Builds a path list from the current workspace directory down to the specified file path.
*/
private List<Path> buildPathList(File baseDir, File filePath) throws IOException {
private static List<Path> buildPathList(VirtualFile baseDir, VirtualFile filePath, String baseRef) throws IOException {
List<Path> pathList = new ArrayList<Path>();
StringBuilder href = new StringBuilder(baseRef);
......@@ -533,8 +517,8 @@ public final class DirectoryBrowserSupport implements HttpResponse {
/**
* Builds the path list and href recursively top-down.
*/
private void buildPathList(File baseDir, File filePath, List<Path> pathList, StringBuilder href) throws IOException {
File parent = filePath.getParentFile();
private static void buildPathList(VirtualFile baseDir, VirtualFile filePath, List<Path> pathList, StringBuilder href) throws IOException {
VirtualFile parent = filePath.getParent();
if (!baseDir.equals(parent)) {
buildPathList(baseDir, parent, pathList, href);
}
......@@ -548,8 +532,6 @@ public final class DirectoryBrowserSupport implements HttpResponse {
pathList.add(path);
}
private static final long serialVersionUID = 1L;
}
private static final Logger LOGGER = Logger.getLogger(DirectoryBrowserSupport.class.getName());
}
......@@ -52,14 +52,12 @@ import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildStep;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.util.FlushProofOutputStream;
import hudson.util.FormApply;
import hudson.util.IOException2;
import hudson.util.LogTaskListener;
import hudson.util.XStream2;
import hudson.util.ProcessTree;
import java.io.BufferedReader;
import java.io.File;
......@@ -121,8 +119,13 @@ import java.io.StringWriter;
import static java.util.logging.Level.*;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.ArtifactManager;
import jenkins.model.ArtifactManagerConfiguration;
import jenkins.model.ArtifactManagerFactory;
import jenkins.model.PeepholePermalink;
import jenkins.model.StandardArtifactManager;
import jenkins.model.RunAction2;
import jenkins.util.VirtualFile;
/**
* A particular execution of {@link Job}.
......@@ -257,6 +260,12 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
*/
private volatile transient RunExecution runner;
/**
* Artifact manager associated with this build, if any.
* @since 1.531
*/
private @CheckForNull ArtifactManager artifactManager;
private static final SimpleDateFormat CANONICAL_ID_FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
public static final ThreadLocal<SimpleDateFormat> ID_FORMATTER = new IDFormatterProvider();
private static final class IDFormatterProvider extends ThreadLocal<SimpleDateFormat> {
......@@ -326,6 +335,9 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
((RunAction) a).onLoad();
}
}
if (artifactManager != null) {
artifactManager.onLoad(this);
}
}
/**
......@@ -964,9 +976,53 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
return new File(project.getBuildDir(),getId());
}
/**
* Gets an object responsible for storing and retrieving build artifacts.
* If {@link #pickArtifactManager} has previously been called on this build,
* and a nondefault manager selected, that will be returned.
* Otherwise (including if we are loading a historical build created prior to this feature) {@link StandardArtifactManager} is used.
* <p>This method should be used when existing artifacts are to be loaded, displayed, or removed.
* If adding artifacts, use {@link #pickArtifactManager} instead.
* @return an appropriate artifact manager
* @since 1.531
*/
public final @Nonnull ArtifactManager getArtifactManager() {
return artifactManager != null ? artifactManager : new StandardArtifactManager(this);
}
/**
* Selects an object responsible for storing and retrieving build artifacts.
* The first time this is called on a running build, {@link ArtifactManagerConfiguration} is checked
* to see if one will handle this build.
* If so, that manager is saved in the build and it will be used henceforth.
* If no manager claimed the build, {@link StandardArtifactManager} is used.
* <p>This method should be used when a build step expects to archive some artifacts.
* If only displaying existing artifacts, use {@link #getArtifactManager} instead.
* @return an appropriate artifact manager
* @throws IOException if a custom manager was selected but the selection could not be saved
* @since 1.531
*/
public final synchronized @Nonnull ArtifactManager pickArtifactManager() throws IOException {
if (artifactManager != null) {
return artifactManager;
} else {
for (ArtifactManagerFactory f : ArtifactManagerConfiguration.get().getArtifactManagerFactories()) {
ArtifactManager mgr = f.managerFor(this);
if (mgr != null) {
artifactManager = mgr;
save();
return mgr;
}
}
return new StandardArtifactManager(this);
}
}
/**
* Gets the directory where the artifacts are archived.
* @deprecated Should only be used from {@link StandardArtifactManager} or subclasses.
*/
@Deprecated
public File getArtifactsDir() {
return new File(getRootDir(),"archive");
}
......@@ -984,7 +1040,11 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
*/
public List<Artifact> getArtifactsUpTo(int n) {
ArtifactList r = new ArtifactList();
addArtifacts(getArtifactsDir(),"","",r,null,n);
try {
addArtifacts(getArtifactManager().root(), "", "", r, null, n);
} catch (IOException x) {
LOGGER.log(Level.WARNING, null, x);
}
r.computeDisplayName();
return r;
}
......@@ -999,18 +1059,17 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
return !getArtifactsUpTo(1).isEmpty();
}
private int addArtifacts( File dir, String path, String pathHref, ArtifactList r, Artifact parent, int upTo ) {
String[] children = dir.list();
if(children==null) return 0;
Arrays.sort(children, String.CASE_INSENSITIVE_ORDER);
private int addArtifacts(VirtualFile dir, String path, String pathHref, ArtifactList r, Artifact parent, int upTo) throws IOException {
VirtualFile[] kids = dir.list();
Arrays.sort(kids);
int n = 0;
for (String child : children) {
for (VirtualFile sub : kids) {
String child = sub.getName();
String childPath = path + child;
String childHref = pathHref + Util.rawEncode(child);
File sub = new File(dir, child);
String length = sub.isFile() ? String.valueOf(sub.length()) : "";
boolean collapsed = (children.length==1 && parent!=null);
boolean collapsed = (kids.length==1 && parent!=null);
Artifact a;
if (collapsed) {
// Collapse single items into parent node where possible:
......@@ -1138,7 +1197,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
@ExportedBean
public class Artifact {
/**
* Relative path name from {@link Run#getArtifactsDir()}
* Relative path name from artifacts root.
*/
@Exported(visibility=3)
public final String relativePath;
......@@ -1181,7 +1240,9 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
/**
* Gets the artifact file.
* @deprecated May not be meaningful with custom artifact managers. Use {@link ArtifactManager#load} with {@link #relativePath} instead.
*/
@Deprecated
public File getFile() {
return new File(getArtifactsDir(),relativePath);
}
......@@ -1933,7 +1994,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
if(Functions.isArtifactsPermissionEnabled()) {
checkPermission(ARTIFACTS);
}
return new DirectoryBrowserSupport(this,new FilePath(getArtifactsDir()), project.getDisplayName()+' '+getDisplayName(), "package.png", true);
return new DirectoryBrowserSupport(this, getArtifactManager().root(), project.getDisplayName() + ' ' + getDisplayName(), "package.png", true);
}
/**
......
......@@ -31,14 +31,17 @@ import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.remoting.VirtualChannel;
import hudson.util.FormValidation;
import java.io.File;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.QueryParameter;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import net.sf.json.JSONObject;
import javax.annotation.Nonnull;
......@@ -123,9 +126,6 @@ public class ArtifactArchiver extends Recorder {
return true;
}
File dir = build.getArtifactsDir();
dir.mkdirs();
listener.getLogger().println(Messages.ArtifactArchiver_ARCHIVING_ARTIFACTS());
try {
FilePath ws = build.getWorkspace();
......@@ -134,7 +134,11 @@ public class ArtifactArchiver extends Recorder {
}
String artifacts = build.getEnvironment(listener).expand(this.artifacts);
if(ws.copyRecursiveTo(artifacts,excludes,new FilePath(dir))==0) {
Map<String,String> files = ws.act(new ListFiles(artifacts, excludes));
if (!files.isEmpty()) {
build.pickArtifactManager().archive(ws, launcher, listener, files);
} else {
Result result = build.getResult();
if (result != null && result.isBetterOrEqualTo(Result.UNSTABLE)) {
// If the build failed, don't complain that there was no matching artifact.
......@@ -165,6 +169,23 @@ public class ArtifactArchiver extends Recorder {
return true;
}
private static final class ListFiles implements FilePath.FileCallable<Map<String,String>> {
private static final long serialVersionUID = 1;
private final String includes, excludes;
ListFiles(String includes, String excludes) {
this.includes = includes;
this.excludes = excludes;
}
@Override public Map<String,String> invoke(File basedir, VirtualChannel channel) throws IOException, InterruptedException {
Map<String,String> r = new HashMap<String,String>();
for (String f : Util.createFileSet(basedir, includes, excludes).getDirectoryScanner().getIncludedFiles()) {
f = f.replace(File.separatorChar, '/');
r.put(f, f);
}
return r;
}
}
@Override
public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) {
if(latestOnly) {
......@@ -176,14 +197,14 @@ public class ArtifactArchiver extends Recorder {
bestResultSoFar = b.getResult();
} else {
// remove old artifacts
File ad = b.getArtifactsDir();
if(ad.exists()) {
listener.getLogger().println(Messages.ArtifactArchiver_DeletingOld(b.getDisplayName()));
try {
Util.deleteRecursive(ad);
} catch (IOException e) {
e.printStackTrace(listener.error(e.getMessage()));
try {
if (b.getArtifactManager().delete()) {
listener.getLogger().println(Messages.ArtifactArchiver_DeletingOld(b.getDisplayName()));
}
} catch (IOException e) {
e.printStackTrace(listener.error(e.getMessage()));
} catch (InterruptedException x) {
x.printStackTrace(listener.error(x.getMessage()));
}
}
}
......
......@@ -26,29 +26,36 @@ public abstract class DirScanner implements Serializable {
*/
public abstract void scan(File dir, FileVisitor visitor) throws IOException;
/**
* @since 1.531
*/
protected final void scanSingle(File f, String relative, FileVisitor visitor) throws IOException {
if (visitor.understandsSymlink()) {
try {
String target;
try {
target = Util.resolveSymlink(f);
} catch (IOException x) { // JENKINS-13202
target = null;
}
if (target != null) {
visitor.visitSymlink(f, target, relative);
return;
}
} catch (InterruptedException e) {
throw (IOException) new InterruptedIOException().initCause(e);
}
}
visitor.visit(f, relative);
}
/**
* Scans everything recursively.
*/
public static class Full extends DirScanner {
private void scan(File f, String path, FileVisitor visitor) throws IOException {
if (f.canRead()) {
if (visitor.understandsSymlink()) {
try {
String target;
try {
target = Util.resolveSymlink(f);
} catch (IOException x) { // JENKINS-13202
target = null;
}
if (target!=null) {
visitor.visitSymlink(f,target,path+f.getName());
return;
}
} catch (InterruptedException e) {
throw (IOException)new InterruptedIOException().initCause(e);
}
}
visitor.visit(f,path+f.getName());
scanSingle(f, path + f.getName(), visitor);
if(f.isDirectory()) {
for( File child : f.listFiles() )
scan(child,path+f.getName()+'/',visitor);
......@@ -113,24 +120,7 @@ public abstract class DirScanner implements Serializable {
DirectoryScanner ds = fs.getDirectoryScanner(new org.apache.tools.ant.Project());
for( String f : ds.getIncludedFiles()) {
File file = new File(dir, f);
if (visitor.understandsSymlink()) {
try {
String target;
try {
target = Util.resolveSymlink(file);
} catch (IOException x) { // JENKINS-13202
target = null;
}
if (target!=null) {
visitor.visitSymlink(file,target,f);
continue;
}
} catch (InterruptedException e) {
throw (IOException)new InterruptedIOException().initCause(e);
}
}
visitor.visit(file,f);
scanSingle(file, f, visitor);
}
}
}
......
/*
* 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.model;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Run;
import hudson.tasks.ArtifactArchiver;
import java.io.IOException;
import java.util.Map;
import jenkins.util.VirtualFile;
/**
* Manager of artifacts for one build.
* @see ArtifactManagerFactory
* @since 1.531
*/
public abstract class ArtifactManager {
/**
* Called when this manager is loaded from disk.
* The selected manager will be persisted inside a build, so the build reference should be {@code transient} (quasi-{@code final}) and restored here.
* @param build a historical build with which this manager was associated
*/
public abstract void onLoad(Run<?,?> build);
/**
* Archive all configured artifacts from a build.
* <p>If called multiple times for the same build, do not delete the old artifacts but keep them all, unless overwritten.
* For example, the XVNC plugin could use this to save {@code screenshot.jpg} if so configured.
* <p>This method is typically invoked on a running build, though e.g. in the case of Maven module builds,
* the build may actually be {@link hudson.model.Run.State#COMPLETED} when this is called
* (since it is the parent build which is still running and performing archiving).
* @param workspace the root directory from which to copy files (typically {@link AbstractBuild#getWorkspace} but not necessarily)
* @param launcher a launcher to use if external processes need to be forked
* @param listener a way to print messages about progress or problems
* @param artifacts map from paths in the archive area to paths relative to {@code workspace} (all paths {@code /}-separated)
* @throws IOException if transfer or copying failed in any way
* @throws InterruptedException if transfer was interrupted
* @see ArtifactArchiver#perform(AbstractBuild, Launcher, BuildListener)
*/
public abstract void archive(FilePath workspace, Launcher launcher, BuildListener listener, Map<String,String> artifacts) throws IOException, InterruptedException;
/**
* Delete all artifacts associated with an earlier build (if any).
* @return true if there was actually anything to delete
* @throws IOException if deletion could not be completed
* @throws InterruptedException if deletion was interrupted
*/
public abstract boolean delete() throws IOException, InterruptedException;
/**
* Returns a representation of the root directory of archived artifacts.
* @return the archive root
*/
public abstract VirtualFile root();
}
/*
* 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.model;
import hudson.Extension;
import hudson.util.DescribableList;
import java.io.IOException;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
/**
* List of configured {@link ArtifactManagerFactory}s.
* @since 1.531
*/
@Extension
public class ArtifactManagerConfiguration extends GlobalConfiguration {
public static ArtifactManagerConfiguration get() {
return Jenkins.getInstance().getInjector().getInstance(ArtifactManagerConfiguration.class);
}
private final DescribableList<ArtifactManagerFactory,ArtifactManagerFactoryDescriptor> artifactManagerFactories = new DescribableList<ArtifactManagerFactory,ArtifactManagerFactoryDescriptor>(this);
public ArtifactManagerConfiguration() {
load();
}
private Object readResolve() {
artifactManagerFactories.setOwner(this);
return this;
}
public DescribableList<ArtifactManagerFactory,ArtifactManagerFactoryDescriptor> getArtifactManagerFactories() {
return artifactManagerFactories;
}
@Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
try {
artifactManagerFactories.rebuildHetero(req, json, ArtifactManagerFactoryDescriptor.all(), "artifactManagerFactories");
return true;
} catch (IOException x) {
throw new FormException(x, "artifactManagerFactories");
}
}
}
/*
* 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.model;
import hudson.ExtensionPoint;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Run;
import javax.annotation.CheckForNull;
import org.kohsuke.stapler.DataBoundConstructor;
/**
* Pluggable ability to manage transfer and/or storage of build artifacts.
* The descriptor should specify at least a display name, and optionally a {@code config} view.
* Since the user can configure this class, you must have a {@link DataBoundConstructor}.
* @see ArtifactManagerConfiguration
* @see ArtifactManagerFactoryDescriptor
* @since 1.531
*/
public abstract class ArtifactManagerFactory extends AbstractDescribableImpl<ArtifactManagerFactory> implements ExtensionPoint {
/**
* Optionally creates a manager for a particular build.
* All configured factories are consulted in sequence; the first manager thus yielded (if any) will be stored in the build.
* {@link StandardArtifactManager} is used as a fallback.
* @param build a running (or recently completed) build ready for {@link ArtifactManager#archive}
* @return a manager, or null if this manager should not handle this kind of project, builds on this kind of slave, etc.
*/
public abstract @CheckForNull ArtifactManager managerFor(Run<?,?> build);
@Override public ArtifactManagerFactoryDescriptor getDescriptor() {
return (ArtifactManagerFactoryDescriptor) super.getDescriptor();
}
}
/*
* 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.model;
import hudson.DescriptorExtensionList;
import hudson.model.Descriptor;
/**
* Definition of a kind of artifact manager.
* @see ArtifactManagerFactory
* @since 1.531
*/
public abstract class ArtifactManagerFactoryDescriptor extends Descriptor<ArtifactManagerFactory> {
public static DescriptorExtensionList<ArtifactManagerFactory,ArtifactManagerFactoryDescriptor> all() {
return Jenkins.getInstance().getDescriptorList(ArtifactManagerFactory.class);
}
}
/*
* 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.model;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.BuildListener;
import hudson.model.Run;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import jenkins.util.VirtualFile;
/**
* Default artifact manager which transfers files over the remoting channel and stores them inside the build directory.
* May be subclassed to provide an artifact manager which uses the standard storage but which only overrides {@link #archive}.
* @since 1.531
*/
public class StandardArtifactManager extends ArtifactManager {
protected transient Run<?,?> build;
public StandardArtifactManager(Run<?,?> build) {
onLoad(build);
}
@Override public final void onLoad(Run<?,?> build) {
this.build = build;
}
@Override public void archive(FilePath workspace, Launcher launcher, BuildListener listener, final Map<String,String> artifacts) throws IOException, InterruptedException {
File dir = getArtifactsDir();
String description = "transfer of " + artifacts.size() + " files"; // TODO improve when just one file
workspace.copyRecursiveTo(new FilePath.ExplicitlySpecifiedDirScanner(artifacts), new FilePath(dir), description);
}
@Override public final boolean delete() throws IOException, InterruptedException {
File ad = getArtifactsDir();
if (!ad.exists()) {
return false;
}
Util.deleteRecursive(ad);
return true;
}
@Override public VirtualFile root() {
return VirtualFile.forFile(getArtifactsDir());
}
@SuppressWarnings("deprecation")
private File getArtifactsDir() {
return build.getArtifactsDir();
}
}
/*
* 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.VirtualChannel;
import hudson.util.DirScanner;
import hudson.util.FileVisitor;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
/**
* 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.
* @see DirectoryBrowserSupport
* @since 1.531
*/
public abstract class VirtualFile implements Comparable<VirtualFile> {
/**
* 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();
}
/**
* 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 VirtualFile() {
@Override public String getName() {
return f.getName();
}
@Override public URI toURI() {
return f.toURI();
}
@Override public VirtualFile getParent() {
return forFile(f.getParentFile());
}
@Override public boolean isDirectory() throws IOException {
return f.isDirectory();
}
@Override public boolean isFile() throws IOException {
return f.isFile();
}
@Override public boolean exists() throws IOException {
return f.exists();
}
@Override public VirtualFile[] list() throws IOException {
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] = forFile(kids[i]);
}
return vfs;
}
@Override public String[] list(String glob) throws IOException {
return new Scanner(glob).invoke(f, null);
}
@Override public VirtualFile child(String name) {
return forFile(new File(f, name));
}
@Override public long length() throws IOException {
return f.length();
}
@Override public long lastModified() throws IOException {
return f.lastModified();
}
@Override public boolean canRead() throws IOException {
return f.canRead();
}
@Override public InputStream open() throws IOException {
return new FileInputStream(f);
}
};
}
/**
* 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 VirtualFile() {
@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 {
return f.read();
}
};
}
private static final class Scanner implements FilePath.FileCallable<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 implements FilePath.FileCallable<Boolean> {
@Override public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
return f.canRead();
}
}
}
/*
* 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.model.ArtifactManagerConfiguration;
f = namespace(lib.FormTagLib);
if (!jenkins.model.ArtifactManagerFactoryDescriptor.all().isEmpty()) {
f.section(title: _("Artifact Management for Builds")) {
f.block() {
f.repeatableHeteroProperty(field: "artifactManagerFactories", hasHeader: true)
}
}
}
......@@ -25,18 +25,17 @@
package hudson.model;
import hudson.Util;
import hudson.model.Run.Artifact;
import hudson.util.StreamTaskListener;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.xml.stream.events.Characters;
import static org.junit.Assert.*;
import org.junit.Test;
......@@ -114,4 +113,37 @@ public class RunTest {
}
}
private List<? extends Run<?, ?>.Artifact> createArtifactList(String... paths) throws Exception {
Run r = new Run(new StubJob(), 0) {};
Run.ArtifactList list = r.new ArtifactList();
for (String p : paths) {
list.add(r.new Artifact(p, p, p, String.valueOf(p.length()), "n" + list.size())); // Assuming all test inputs don't need urlencoding
}
list.computeDisplayName();
return list;
}
@Test
public void artifactListDisambiguation1() throws Exception {
List<? extends Run<?, ?>.Artifact> a = createArtifactList("a/b/c.xml", "d/f/g.xml", "h/i/j.xml");
assertEquals(a.get(0).getDisplayPath(), "c.xml");
assertEquals(a.get(1).getDisplayPath(), "g.xml");
assertEquals(a.get(2).getDisplayPath(), "j.xml");
}
@Test
public void artifactListDisambiguation2() throws Exception {
List<? extends Run<?, ?>.Artifact> a = createArtifactList("a/b/c.xml", "d/f/g.xml", "h/i/g.xml");
assertEquals(a.get(0).getDisplayPath(), "c.xml");
assertEquals(a.get(1).getDisplayPath(), "f/g.xml");
assertEquals(a.get(2).getDisplayPath(), "i/g.xml");
}
@Test
public void artifactListDisambiguation3() throws Exception {
List<? extends Run<?, ?>.Artifact> a = createArtifactList("a.xml", "a/a.xml");
assertEquals(a.get(0).getDisplayPath(), "a.xml");
assertEquals(a.get(1).getDisplayPath(), "a/a.xml");
}
}
......@@ -25,6 +25,7 @@ package hudson.maven;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.maven.reporters.MavenArtifactRecord;
import hudson.maven.reporters.SurefireArchiver;
import hudson.maven.reporters.TestFailureDetector;
......@@ -43,6 +44,7 @@ import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.scm.ChangeLogSet;
import hudson.scm.ChangeLogSet.Entry;
import hudson.tasks.BuildWrapper;
......@@ -71,12 +73,15 @@ import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import jenkins.model.ArtifactManager;
import jenkins.mvn.SettingsProvider;
......@@ -402,6 +407,8 @@ public class MavenBuild extends AbstractMavenBuild<MavenModule,MavenBuild> {
class ProxyImpl implements MavenBuildProxy, Serializable {
private static final long serialVersionUID = 8865133776526671879L;
private final Map<String,String> artifacts = new LinkedHashMap<String,String>();
public <V, T extends Throwable> V execute(BuildCallable<V, T> program) throws T, IOException, InterruptedException {
return program.call(MavenBuild.this);
}
......@@ -429,10 +436,47 @@ public class MavenBuild extends AbstractMavenBuild<MavenModule,MavenBuild> {
return new FilePath(MavenBuild.this.getParent().getParent().getRootDir());
}
/**
* @deprecated Does not work with {@link ArtifactManager}.
*/
@Deprecated
public FilePath getArtifactsDir() {
return new FilePath(MavenBuild.this.getArtifactsDir());
}
@Override public void queueArchiving(String artifactPath, String artifact) {
artifacts.put(artifactPath, artifact);
}
void performArchiving(Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
for (Map.Entry<String,String> e : artifacts.entrySet()) {
listener.getLogger().println("[JENKINS] Archiving " + e.getValue() + " to " + e.getKey());
}
ArtifactManager am = pickArtifactManager();
FilePath ws = getWorkspace();
Map<String,String> artifactsInsideWorkspace = new LinkedHashMap<String,String>();
String prefix = ws.act(new CanonicalPath()) + '/'; // try to relativize paths to workspace
Iterator<Map.Entry<String,String>> it = artifacts.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String,String> e = it.next();
String p = new FilePath(ws, e.getValue()).act(new CanonicalPath());
if (!p.startsWith(prefix)) {
listener.getLogger().println(p + " is not inside " + prefix + "; will archive in a separate pass");
continue;
}
artifactsInsideWorkspace.put(e.getKey(), p.substring(prefix.length()));
it.remove();
}
if (!artifactsInsideWorkspace.isEmpty()) {
am.archive(ws, launcher, listener, artifactsInsideWorkspace);
}
// Now handle other files outside the workspace, if any.
for (Map.Entry<String,String> e : artifacts.entrySet()) {
FilePath f = new FilePath(ws, e.getValue());
am.archive(f.getParent(), launcher, listener, Collections.singletonMap(e.getKey(), f.getName()));
}
}
public void setResult(Result result) {
MavenBuild.this.setResult(result);
}
......@@ -476,6 +520,13 @@ public class MavenBuild extends AbstractMavenBuild<MavenModule,MavenBuild> {
}
}
private static final class CanonicalPath implements FilePath.FileCallable<String> {
private static final long serialVersionUID = 1;
@Override public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
return f.getCanonicalPath().replace(File.separatorChar, '/');
}
}
public class ProxyImpl2 extends ProxyImpl implements MavenBuildProxy2 {
private static final long serialVersionUID = -3377221864644014218L;
......@@ -761,9 +812,11 @@ public class MavenBuild extends AbstractMavenBuild<MavenModule,MavenBuild> {
{
boolean normalExit = false;
try {
ProxyImpl proxy = new ProxyImpl();
Result r = process.call(new Builder(
listener,new ProxyImpl(),
listener, proxy,
getProject(), margs.toList(), systemProps));
proxy.performArchiving(launcher, listener);
normalExit = true;
return r;
} finally {
......
......@@ -25,6 +25,7 @@ package hudson.maven;
import hudson.FilePath;
import hudson.model.Result;
import hudson.remoting.Asynchronous;
import hudson.remoting.Callable;
import hudson.remoting.DelegatingCallable;
......@@ -32,6 +33,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.util.Calendar;
import java.util.List;
import jenkins.model.ArtifactManager;
/**
* Remoting proxy interface for {@link MavenReporter}s to talk to {@link MavenBuild}
......@@ -93,10 +95,20 @@ public interface MavenBuildProxy {
FilePath getModuleSetRootDir();
/**
* @see MavenBuild#getArtifactsDir()
* @deprecated Does not work with {@link ArtifactManager}.
*/
@Deprecated
FilePath getArtifactsDir();
/**
* @param artifactPath a relative {@code /}-separated path
* @param artifact absolute path name on the slave in the workspace
* @see ArtifactManager#archive
* @since 1.531
*/
@Asynchronous
void queueArchiving(String artifactPath, String artifact);
/**
* @see MavenBuild#setResult(Result)
*/
......@@ -211,6 +223,10 @@ public interface MavenBuildProxy {
return core.getArtifactsDir();
}
@Override public void queueArchiving(String artifactPath, String artifact) {
core.queueArchiving(artifactPath, artifact);
}
public void setResult(Result result) {
core.setResult(result);
}
......
......@@ -811,6 +811,9 @@ public class MavenModuleSetBuild extends AbstractMavenBuild<MavenModuleSet,Maven
mpa = new MavenProbeAction(project,process.channel);
addAction(mpa);
r = process.call(builder);
for (ProxyImpl2 proxy : proxies.values()) {
proxy.performArchiving(launcher, listener);
}
return r;
} finally {
builder.end(launcher);
......
......@@ -23,7 +23,6 @@
*/
package hudson.maven.reporters;
import hudson.FilePath;
import hudson.Util;
import hudson.maven.MavenBuild;
import hudson.maven.MavenBuildProxy;
......@@ -34,7 +33,6 @@ import hudson.model.Run;
import hudson.util.LRUStringConverter;
import jenkins.model.Jenkins;
import hudson.util.HttpResponses;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.handler.ArtifactHandler;
......@@ -48,10 +46,16 @@ import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Map;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
/**
* Captures information about an artifact created by Maven and archived by
......@@ -158,6 +162,7 @@ public final class MavenArtifact implements Serializable {
/**
* Creates a Maven {@link Artifact} back from the persisted data.
* {@link Artifact#getFile} should be deleted when you are finished as it is a temporary copy.
*/
public Artifact toArtifact(ArtifactHandlerManager handlerManager, ArtifactFactory factory, MavenBuild build) throws IOException {
// Hack: presence of custom ArtifactHandler during builds could influence the file extension
......@@ -194,11 +199,18 @@ public final class MavenArtifact implements Serializable {
/**
* Obtains the {@link File} representing the archived artifact.
* This is a temporary copy which you should delete when finished.
* @throws FileNotFoundException if the archived artifact was missing
*/
public File getFile(MavenBuild build) throws IOException {
File f = new File(new File(new File(new File(build.getArtifactsDir(), groupId), artifactId), version), canonicalName);
if(!f.exists())
throw new IOException("Archived artifact is missing: "+f);
File f = File.createTempFile("jenkins-", canonicalName);
f.deleteOnExit();
OutputStream os = new FileOutputStream(f);
try {
Util.copyStreamAndClose(build.getArtifactManager().root().child(artifactPath()).open(), os);
} finally {
os.close();
}
return f;
}
......@@ -207,12 +219,17 @@ public final class MavenArtifact implements Serializable {
*
* TODO: figure out how to make this URL more discoverable to the remote API.
*/
public HttpResponse doFile(@AncestorInPath MavenArtifactRecord parent) throws IOException {
return HttpResponses.staticResource(getFile(parent.parent));
public HttpResponse doFile(final @AncestorInPath MavenArtifactRecord parent) throws IOException {
return new HttpResponse() {
@Override public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
rsp.setContentType("application/octet-stream");
Util.copyStreamAndClose(parent.parent.getArtifactManager().root().child(artifactPath()).open(), rsp.getCompressedOutputStream(req));
}
};
}
private FilePath getArtifactArchivePath(MavenBuildProxy build, String groupId, String artifactId, String version) {
return build.getArtifactsDir().child(groupId).child(artifactId).child(version).child(canonicalName);
private String artifactPath() {
return groupId + '/' + artifactId + '/' + version + '/' + canonicalName;
}
/**
......@@ -223,58 +240,7 @@ public final class MavenArtifact implements Serializable {
LOGGER.fine("Archiving disabled - not archiving " + file);
}
else {
FilePath target = getArtifactArchivePath(build,groupId,artifactId,version);
FilePath origin = new FilePath(file);
if (!target.exists()) {
listener.getLogger().println("[JENKINS] Archiving "+ file+" to "+target);
origin.copyTo(target);
} else if (!origin.digest().equals(target.digest())) {
listener.getLogger().println("[JENKINS] Re-archiving "+file);
origin.copyTo(target);
} else {
LOGGER.fine("Not actually archiving "+origin+" due to digest match");
}
/* debug probe to investigate "missing artifact" problem typically seen like this:
ERROR: Asynchronous execution failure
java.util.concurrent.ExecutionException: java.io.IOException: Archived artifact is missing: /files/hudson/server/jobs/glassfish-v3/modules/org.glassfish.build$maven-glassfish-extension/builds/2008-04-02_10-17-15/archive/org.glassfish.build/maven-glassfish-extension/1.0-SNAPSHOT/maven-glassfish-extension-1.0-SNAPSHOT.jar
at hudson.remoting.Channel$1.adapt(Channel.java:423)
at hudson.remoting.Channel$1.adapt(Channel.java:418)
at hudson.remoting.FutureAdapter.get(FutureAdapter.java:32)
at hudson.maven.MavenBuilder.call(MavenBuilder.java:140)
at hudson.maven.MavenModuleSetBuild$Builder.call(MavenModuleSetBuild.java:476)
at hudson.maven.MavenModuleSetBuild$Builder.call(MavenModuleSetBuild.java:422)
at hudson.remoting.UserRequest.perform(UserRequest.java:69)
at hudson.remoting.UserRequest.perform(UserRequest.java:23)
at hudson.remoting.Request$2.run(Request.java:200)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:417)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:269)
at java.util.concurrent.FutureTask.run(FutureTask.java:123)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675)
at java.lang.Thread.run(Thread.java:595)
Caused by: java.io.IOException: Archived artifact is missing: /files/hudson/server/jobs/glassfish-v3/modules/org.glassfish.build$maven-glassfish-extension/builds/2008-04-02_10-17-15/archive/org.glassfish.build/maven-glassfish-extension/1.0-SNAPSHOT/maven-glassfish-extension-1.0-SNAPSHOT.jar
at hudson.maven.reporters.MavenArtifact.getFile(MavenArtifact.java:147)
at hudson.maven.reporters.MavenArtifact.toArtifact(MavenArtifact.java:126)
at hudson.maven.reporters.MavenArtifactRecord.install(MavenArtifactRecord.java:115)
at hudson.maven.reporters.MavenArtifactArchiver$1.call(MavenArtifactArchiver.java:81)
at hudson.maven.reporters.MavenArtifactArchiver$1.call(MavenArtifactArchiver.java:71)
at hudson.maven.MavenBuild$ProxyImpl.execute(MavenBuild.java:255)
at hudson.maven.MavenBuildProxy$Filter$AsyncInvoker.call(MavenBuildProxy.java:177)
at hudson.remoting.UserRequest.perform(UserRequest.java:69)
at hudson.remoting.UserRequest.perform(UserRequest.java:23)
at hudson.remoting.Request$2.run(Request.java:200)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:441)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
at java.util.concurrent.FutureTask.run(FutureTask.java:138)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:885)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:907)
at java.lang.Thread.run(Thread.java:619)
*/
if(!target.exists())
throw new AssertionError("Just copied "+file+" to "+target+" but now I can't find it");
build.queueArchiving(artifactPath(), file.getAbsolutePath());
}
}
......
......@@ -24,7 +24,6 @@
package hudson.maven.reporters;
import hudson.Extension;
import hudson.FilePath;
import hudson.Util;
import hudson.maven.*;
import hudson.model.BuildListener;
......@@ -158,9 +157,9 @@ public class MavenArtifactArchiver extends MavenReporter {
for (File assembly : assemblies) {
if(mavenArtifacts.contains(assembly))
continue; // looks like this is already archived
FilePath target = build.getArtifactsDir().child(assembly.getName());
String target = assembly.getName();
listener.getLogger().println("[JENKINS] Archiving "+ assembly+" to "+target);
new FilePath(assembly).copyTo(target);
build.queueArchiving(target, assembly.getAbsolutePath());
// TODO: fingerprint
}
}
......
......@@ -28,6 +28,7 @@ import hudson.maven.RedeployPublisher.WrappedArtifactRepository;
import hudson.model.AbstractItem;
import hudson.model.Action;
import hudson.model.TaskListener;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
......@@ -39,8 +40,6 @@ import org.apache.maven.artifact.deployer.ArtifactDeployer;
import org.apache.maven.artifact.deployer.ArtifactDeploymentException;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
import org.apache.maven.artifact.installer.ArtifactInstallationException;
import org.apache.maven.artifact.installer.ArtifactInstaller;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.metadata.GroupRepositoryMetadata;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
......@@ -171,9 +170,11 @@ public class MavenArtifactRecord extends MavenAbstractArtifactRecord<MavenBuild>
((WrappedArtifactRepository) deploymentRepository).setUniqueVersion(true);
}
Artifact main = mainArtifact.toArtifact(handlerManager, artifactFactory, parent);
if (!isPOM())
main.addMetadata(new ProjectArtifactMetadata(main, pomArtifact.getFile(parent)));
File pomFile = null;
if (!isPOM()) {
pomFile = pomArtifact.getFile(parent);
main.addMetadata(new ProjectArtifactMetadata(main, pomFile));
}
if (main.getType().equals("maven-plugin")) {
GroupRepositoryMetadata metadata = new GroupRepositoryMetadata(main.getGroupId());
String goalPrefix = PluginDescriptor.getGoalPrefixFromArtifactId(main.getArtifactId());
......@@ -188,30 +189,18 @@ public class MavenArtifactRecord extends MavenAbstractArtifactRecord<MavenBuild>
// deploy the main artifact. This also deploys the POM
logger.println(Messages.MavenArtifact_DeployingMainArtifact(main.getFile().getName()));
deployer.deploy(main.getFile(), main, deploymentRepository, embedder.getLocalRepository());
main.getFile().delete();
if (pomFile != null) {
pomFile.delete();
}
for (MavenArtifact aa : attachedArtifacts) {
Artifact a = aa.toArtifact(handlerManager, artifactFactory, parent);
logger.println(Messages.MavenArtifact_DeployingMainArtifact(a.getFile().getName()));
deployer.deploy(a.getFile(), a, deploymentRepository, embedder.getLocalRepository());
a.getFile().delete();
}
}
/**
* Installs the artifact to the local Maven repository.
*/
public void install(MavenEmbedder embedder) throws MavenEmbedderException, IOException, ComponentLookupException, ArtifactInstallationException {
ArtifactHandlerManager handlerManager = embedder.lookup(ArtifactHandlerManager.class);
ArtifactInstaller installer = embedder.lookup(ArtifactInstaller.class);
ArtifactFactory factory = embedder.lookup(ArtifactFactory.class);
Artifact main = mainArtifact.toArtifact(handlerManager,factory,parent);
if(!isPOM())
main.addMetadata(new ProjectArtifactMetadata(main,pomArtifact.getFile(parent)));
installer.install(mainArtifact.getFile(parent),main,embedder.getLocalRepository());
for (MavenArtifact aa : attachedArtifacts)
installer.install(aa.getFile(parent), aa.toArtifact(handlerManager, factory, parent), embedder.getLocalRepository());
}
public void recordFingerprints() throws IOException {
// record fingerprints
......
......@@ -246,6 +246,8 @@ public class SurefireArchiverUnitTest {
return null;
}
@Override public void queueArchiving(String artifactPath, String artifact) {}
@Override
public void setResult(Result result) {
}
......
package hudson.maven;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.FilePath;
import hudson.Functions;
import org.junit.Assert;
import org.jvnet.hudson.test.Bug;
......@@ -17,13 +18,21 @@ import hudson.model.BuildListener;
import hudson.model.Job;
import hudson.model.PermalinkProjectAction;
import hudson.model.Result;
import hudson.model.Run;
import hudson.tasks.Fingerprinter.FingerprintAction;
import hudson.tasks.Maven.MavenInstallation;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import jenkins.model.ArtifactManager;
import java.util.Set;
import java.util.TreeSet;
import jenkins.model.ArtifactManagerConfiguration;
import jenkins.model.ArtifactManagerFactory;
import jenkins.util.VirtualFile;
import static org.junit.Assert.*;
import org.junit.Assume;
import org.junit.Ignore;
......@@ -464,6 +473,83 @@ public class MavenMultiModuleTest {
modulesPage.getAnchorByText(m.getDisplayName()).openLinkInNewWindow();
}
@Bug(17236)
@Test public void artifactArchiving() throws Exception {
ArtifactManagerConfiguration.get().getArtifactManagerFactories().add(new TestAMF());
j.configureDefaultMaven(); // using Maven 2 so we can test single-module builds
MavenModuleSet mms = j.createMavenProject();
mms.setScm(new ExtractResourceSCM(getClass().getResource("maven-multimod.zip")));
mms.setAssignedNode(j.createOnlineSlave());
j.buildAndAssertSuccess(mms);
// We want all the artifacts in a given module to be archived in one operation. But modules are archived separately.
Map<String,Map<String,FilePath>> expected = new TreeMap<String,Map<String,FilePath>>();
FilePath ws = mms.getModule("org.jvnet.hudson.main.test.multimod$multimod-top").getBuildByNumber(1).getWorkspace();
expected.put("org.jvnet.hudson.main.test.multimod:multimod-top", Collections.singletonMap("org.jvnet.hudson.main.test.multimod/multimod-top/1.0-SNAPSHOT/multimod-top-1.0-SNAPSHOT.pom", new FilePath(ws.getChannel(), "…/org/jvnet/hudson/main/test/multimod/multimod-top/1.0-SNAPSHOT/multimod-top-1.0-SNAPSHOT.pom")));
for (String module : new String[] {"moduleA", "moduleB", "moduleC"}) {
Map<String,FilePath> m = new TreeMap<String,FilePath>();
ws = mms.getModule("org.jvnet.hudson.main.test.multimod$" + module).getBuildByNumber(1).getWorkspace();
m.put("org.jvnet.hudson.main.test.multimod/" + module + "/1.0-SNAPSHOT/" + module + "-1.0-SNAPSHOT.pom", ws.child("pom.xml"));
m.put("org.jvnet.hudson.main.test.multimod/" + module + "/1.0-SNAPSHOT/" + module + "-1.0-SNAPSHOT.jar", ws.child("target/" + module + "-1.0-SNAPSHOT.jar"));
expected.put("org.jvnet.hudson.main.test.multimod:" + module, m);
}
assertEquals(expected.toString(), TestAM.archivings.toString()); // easy to read
assertEquals(expected, TestAM.archivings); // compares also FileChannel
// Also check single-module build.
expected.clear();
TestAM.archivings.clear();
MavenBuild isolated = j.buildAndAssertSuccess(mms.getModule("org.jvnet.hudson.main.test.multimod$moduleA"));
assertEquals(2, isolated.number);
Map<String,FilePath> m = new TreeMap<String,FilePath>();
ws = isolated.getWorkspace();
m.put("org.jvnet.hudson.main.test.multimod/moduleA/1.0-SNAPSHOT/moduleA-1.0-SNAPSHOT.pom", ws.child("pom.xml"));
m.put("org.jvnet.hudson.main.test.multimod/moduleA/1.0-SNAPSHOT/moduleA-1.0-SNAPSHOT.jar", ws.child("target/moduleA-1.0-SNAPSHOT.jar"));
expected.put("org.jvnet.hudson.main.test.multimod:moduleA", m);
assertEquals(expected, TestAM.archivings);
}
public static final class TestAMF extends ArtifactManagerFactory {
@Override public ArtifactManager managerFor(Run<?,?> build) {
return new TestAM(build);
}
}
public static final class TestAM extends ArtifactManager {
static final Map</* module name */String,Map</* archive path */String,/* file in workspace */FilePath>> archivings = new TreeMap<String,Map<String,FilePath>>();
transient Run<?,?> build;
TestAM(Run<?,?> build) {
onLoad(build);
}
@Override public void onLoad(Run<?, ?> build) {
this.build = build;
}
@Override public void archive(FilePath workspace, Launcher launcher, BuildListener listener, Map<String,String> artifacts) throws IOException, InterruptedException {
String name = build.getParent().getName();
if (archivings.containsKey(name)) {
// Would be legitimate only if some archived files for a given module were outside workspace, such as repository parent POM, *and* others were inside, which is not the case in this test.
throw new IOException("repeated archiving to " + name);
}
Map<String,FilePath> m = new TreeMap<String,FilePath>();
for (Map.Entry<String,String> e : artifacts.entrySet()) {
FilePath f = workspace.child(e.getValue());
if (f.exists()) {
if (f.getRemote().replace('\\', '/').contains("/org/jvnet/hudson/main/test/")) {
// Inside the local repository. Hard to know exactly what that path might be, so just mask it out.
f = new FilePath(f.getChannel(), f.getRemote().replaceFirst("^.+(?=[/\\\\]org[/\\\\]jvnet[/\\\\]hudson[/\\\\]main[/\\\\]test[/\\\\])", "…"));
}
m.put(e.getKey(), f);
} else {
throw new IOException("no such file " + f);
}
}
archivings.put(name, m);
}
@Override public boolean delete() throws IOException, InterruptedException {
throw new IOException();
}
@Override public VirtualFile root() {
throw new UnsupportedOperationException();
}
}
/*
@Test public void parallelMultiModMavenWsExists() throws Exception {
configureDefaultMaven();
......
......@@ -26,9 +26,6 @@ package hudson.model;
import java.net.HttpURLConnection;
import java.util.Collection;
import java.util.Collections;
import static org.junit.Assert.*;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Bug;
......@@ -41,37 +38,6 @@ public class RunTest {
@Rule public JenkinsRule j = new JenkinsRule();
private List<? extends Run<?,?>.Artifact> createArtifactList(String... paths) throws Exception {
FreeStyleProject prj = j.createFreeStyleProject();
FreeStyleBuild r = prj.scheduleBuild2(0).get();
Run<FreeStyleProject,FreeStyleBuild>.ArtifactList list = r.new ArtifactList();
for (String p : paths) {
list.add(r.new Artifact(p,p,p,String.valueOf(p.length()),"n"+list.size())); // Assuming all test inputs don't need urlencoding
}
list.computeDisplayName();
return list;
}
@Test public void artifactListDisambiguation1() throws Exception {
List<? extends Run<?, ?>.Artifact> a = createArtifactList("a/b/c.xml", "d/f/g.xml", "h/i/j.xml");
assertEquals(a.get(0).getDisplayPath(),"c.xml");
assertEquals(a.get(1).getDisplayPath(),"g.xml");
assertEquals(a.get(2).getDisplayPath(),"j.xml");
}
@Test public void artifactListDisambiguation2() throws Exception {
List<? extends Run<?, ?>.Artifact> a = createArtifactList("a/b/c.xml", "d/f/g.xml", "h/i/g.xml");
assertEquals(a.get(0).getDisplayPath(),"c.xml");
assertEquals(a.get(1).getDisplayPath(),"f/g.xml");
assertEquals(a.get(2).getDisplayPath(),"i/g.xml");
}
@Test public void artifactListDisambiguation3() throws Exception {
List<? extends Run<?, ?>.Artifact> a = createArtifactList("a.xml","a/a.xml");
assertEquals(a.get(0).getDisplayPath(),"a.xml");
assertEquals(a.get(1).getDisplayPath(),"a/a.xml");
}
@Bug(17935)
@Test public void getDynamicInvisibleTransientAction() throws Exception {
TransientBuildActionFactory.all().add(0, new TransientBuildActionFactory() {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册