Skip to content
Permalink
Browse files

[JENKINS-17236] Initial version of ArtifactManager (no Maven support …

…yet).
  • Loading branch information...
jglick committed Mar 15, 2013
1 parent 2ef5a7c commit e1eea67bb2fa1a356492e91313d84a523f441b34
@@ -34,7 +34,6 @@
import hudson.EnvVars;
import hudson.ExtensionPoint;
import hudson.FeedAdapter;
import hudson.FilePath;
import hudson.Util;
import hudson.XmlFile;
import hudson.cli.declarative.CLIMethod;
@@ -75,7 +74,6 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
@@ -110,6 +108,9 @@
import org.kohsuke.stapler.export.ExportedBean;

import com.thoughtworks.xstream.XStream;
import hudson.ExtensionList;
import hudson.Launcher;
import hudson.model.Run.RunExecution;
import java.io.ByteArrayInputStream;
import org.kohsuke.stapler.interceptor.RequirePOST;

@@ -119,6 +120,8 @@
import static java.util.logging.Level.*;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.ArtifactManager;
import jenkins.model.StandardArtifactManager;

/**
* A particular execution of {@link Job}.
@@ -253,6 +256,12 @@
*/
private volatile transient RunExecution runner;

/**
* {@link ArtifactManager#id} of the artifact manager associated with this build, if any.
* @since XXX
*/
private @CheckForNull String artifactManager;

private static final SimpleDateFormat CANONICAL_ID_FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
protected static final ThreadLocal<SimpleDateFormat> ID_FORMATTER = new IDFormatterProvider();
private static final class IDFormatterProvider extends ThreadLocal<SimpleDateFormat> {
@@ -943,9 +952,57 @@ public File getRootDir() {
return new File(project.getBuildDir(),getId());
}

public final synchronized ArtifactManager getArtifactManager() {
ExtensionList<ArtifactManager> managers = Jenkins.getInstance().getExtensionList(ArtifactManager.class);
if (artifactManager != null) {
for (ArtifactManager mgr : managers) {
if (mgr.id().equals(artifactManager)) {
return mgr;
}
}
}
if (isBuilding()) {
for (ArtifactManager mgr : managers) {
if (mgr.appliesTo(this)) {
artifactManager = mgr.id();
try {
save();
} catch (IOException x) {
LOGGER.log(Level.WARNING, "could not persist artifactManager", x);
}
return mgr;
}
}
} else { // compatibility: completed build prior to introduction of ArtifactManager
ArtifactManager mgr = StandardArtifactManager.instance();
if (mgr.appliesTo(this)) {
return mgr;
}
}
return new ArtifactManager() { // fallback for non-AbstractProject runs
@Override public boolean appliesTo(Run<?, ?> build) {
return true;
}
@Override public int archive(Run<?,?> build, Launcher launcher, BuildListener listener, String artifacts, String excludes) throws IOException, InterruptedException {
throw new IOException("not supported");
}
@Override public boolean deleteArtifacts(Run<?,?> build) throws IOException, InterruptedException {
throw new IOException("not supported");
}
@Override public HttpResponse browseArtifacts(Run<?, ?> build) {
return HttpResponses.notFound();
}
@Override public <JobT extends Job<JobT,RunT>, RunT extends Run<JobT,RunT>> Run<JobT,RunT>.ArtifactList getArtifactsUpTo(Run<JobT,RunT> build, int n) {
return build.new ArtifactList();
}
};
}

/**
* Gets the directory where the artifacts are archived.
* @deprecated Should only be used from {@link StandardArtifactManager} or managers delegating to it for storage.
*/
@Deprecated
public File getArtifactsDir() {
return new File(getRootDir(),"archive");
}
@@ -962,10 +1019,7 @@ public File getArtifactsDir() {
* Gets the first N artifacts.
*/
public List<Artifact> getArtifactsUpTo(int n) {
ArtifactList r = new ArtifactList();
addArtifacts(getArtifactsDir(),"","",r,null,n);
r.computeDisplayName();
return r;
return getArtifactManager().getArtifactsUpTo(this, n);
}

/**
@@ -978,44 +1032,6 @@ public boolean getHasArtifacts() {
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);

int n = 0;
for (String child : children) {
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);
Artifact a;
if (collapsed) {
// Collapse single items into parent node where possible:
a = new Artifact(parent.getFileName() + '/' + child, childPath,
sub.isDirectory() ? null : childHref, length,
parent.getTreeNodeId());
r.tree.put(a, r.tree.remove(parent));
} else {
// Use null href for a directory:
a = new Artifact(child, childPath,
sub.isDirectory() ? null : childHref, length,
"n" + ++r.idSeq);
r.tree.put(a, parent!=null ? parent.getTreeNodeId() : null);
}
if (sub.isDirectory()) {
n += addArtifacts(sub, childPath + '/', childHref + '/', r, a, upTo-n);
if (n>=upTo) break;
} else {
// Don't store collapsed path in ArrayList (for correct data in external API)
r.add(collapsed ? new Artifact(child, a.relativePath, a.href, length, a.treeNodeId) : a);
if (++n>=upTo) break;
}
}
return n;
}

/**
* Maximum number of artifacts to list before using switching to the tree view.
*/
@@ -1035,7 +1051,6 @@ private int addArtifacts( File dir, String path, String pathHref, ArtifactList r
* Contains Artifact objects for directories and files (the ArrayList contains only files).
*/
private LinkedHashMap<Artifact,String> tree = new LinkedHashMap<Artifact,String>();
private int idSeq = 0;

public Map<Artifact,String> getTree() {
return tree;
@@ -1115,7 +1130,7 @@ private String combineLast(String[] token, int n) {
* A build artifact.
*/
@ExportedBean
public class Artifact {
public final class Artifact {
/**
* Relative path name from {@link Run#getArtifactsDir()}
*/
@@ -1150,7 +1165,10 @@ private String combineLast(String[] token, int n) {
*/
private String length;

/*package for test*/ Artifact(String name, String relativePath, String href, String len, String treeNodeId) {
/**
* @since XXX
*/
public Artifact(String name, String relativePath, String href, String len, String treeNodeId) {
this.name = name;
this.relativePath = relativePath;
this.href = href;
@@ -1160,7 +1178,9 @@ private String combineLast(String[] token, int n) {

/**
* Gets the artifact file.
* @deprecated May not be meaningful with custom artifact managers.
*/
@Deprecated
public File getFile() {
return new File(getArtifactsDir(),relativePath);
}
@@ -1866,11 +1886,11 @@ private Summary determineDetailedUnstableSummary(Boolean worseOverride) {
/**
* Serves the artifacts.
*/
public DirectoryBrowserSupport doArtifact() {
public HttpResponse doArtifact() {
if(Functions.isArtifactsPermissionEnabled()) {
checkPermission(ARTIFACTS);
}
return new DirectoryBrowserSupport(this,new FilePath(getArtifactsDir()), project.getDisplayName()+' '+getDisplayName(), "package.png", true);
return getArtifactManager().browseArtifacts(this);
}

/**
@@ -37,7 +37,6 @@
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.QueryParameter;

import java.io.File;
import java.io.IOException;

import net.sf.json.JSONObject;
@@ -101,9 +100,6 @@ public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListene
build.setResult(Result.FAILURE);
return true;
}

File dir = build.getArtifactsDir();
dir.mkdirs();

listener.getLogger().println(Messages.ArtifactArchiver_ARCHIVING_ARTIFACTS());
try {
@@ -113,7 +109,8 @@ public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListene
}

String artifacts = build.getEnvironment(listener).expand(this.artifacts);
if(ws.copyRecursiveTo(artifacts,excludes,new FilePath(dir))==0) {

if (build.getArtifactManager().archive(build, launcher, listener, artifacts, excludes) == 0) {
if(build.getResult().isBetterOrEqualTo(Result.UNSTABLE)) {
// If the build failed, don't complain that there was no matching artifact.
// The build probably didn't even get to the point where it produces artifacts.
@@ -152,14 +149,14 @@ public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) {
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 (build.getArtifactManager().deleteArtifacts(build)) {
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()));
}
}
b = b.getPreviousBuild();
@@ -0,0 +1,103 @@
/*
* 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.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.DirectoryBrowserSupport;
import hudson.model.Job;
import hudson.model.Run;
import hudson.tasks.ArtifactArchiver;
import java.io.IOException;
import javax.annotation.CheckForNull;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;

/**
* Pluggable ability to manage transfer and/or storage of build artifacts.
* @since XXX
*/
public abstract class ArtifactManager implements ExtensionPoint {

/**
* A unique ID for this manager among all registered managers.
* @return by default, the class name
* @see #appliesTo
*/
public String id() {
return getClass().getName();
}

/**
* Permits the manager to restrict its operation to certain kinds of projects, slaves, etc.
* @param build a running build ready for {@link #archive} (the choice of manager will be remembered via {@link #id})
* @return true to handle this build, false to continue the search
*/
public abstract boolean appliesTo(Run<?,?> build);

/**
* Archive all configured artifacts from a build.
* @param build the build which may have produced archivable files
* @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 comma- or space-separated list of patterns of files/directories to be archived (Ant format, any variables already substituted)
* @param excludes patterns of files to be excluded from the artifact list (Ant format, may be null for no excludes)
* @return the number of files actually archived (may be zero)
* @throws IOException if transfer or copying failed in any way
* @see ArtifactArchiver#perform(AbstractBuild, Launcher, BuildListener)
*/
public abstract int archive(Run<?,?> build, Launcher launcher, BuildListener listener, String artifacts, @CheckForNull String excludes) throws IOException, InterruptedException;

/**
* Delete all artifacts associated with an earlier build (if any).
* @param build a build which may have been previously passed to {@link #archive}
* @return true if there was actually anything to delete
* @throws IOException if deletion could not be completed
*/
public abstract boolean deleteArtifacts(Run<?,?> build) throws IOException, InterruptedException;

/**
* Displays all artifacts of a build in a web display.
* (There is no need to check {@link Run#ARTIFACTS} permission.)
* The response should also be capable of handling display of individual artifacts or subdirectories as per {@link StaplerRequest#getRestOfPath},
* and serving {@code *fingerprint*} URLs as {@link DirectoryBrowserSupport} does.
* @param build a build for which artifacts may have been archived
* @return some web page
*/
public abstract HttpResponse browseArtifacts(Run<?,?> build);

/**
* Loads a manifest of some or all artifact records.
* @param build a build which may have artifacts
* @param n a maximum number of items to return
* @return a list of artifact records
*/
public abstract <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,RunT>> Run<JobT,RunT>.ArtifactList getArtifactsUpTo(Run<JobT,RunT> build, int n);

// XXX way to open an Artifact as e.g. an InputStream

}

0 comments on commit e1eea67

Please sign in to comment.
You can’t perform that action at this time.