diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index 0c0aa33f95..f00b42bd04 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -34,6 +34,7 @@ import hudson.BulkChange; import hudson.EnvVars; import hudson.ExtensionPoint; import hudson.FeedAdapter; +import hudson.FilePath; import hudson.Util; import hudson.XmlFile; import hudson.cli.declarative.CLIMethod; @@ -1035,7 +1036,16 @@ public abstract class Run ,RunT extends Run getArtifactsUpTo(int n) { - return getArtifactManager().getArtifactsUpTo(n); + ArtifactList r = new ArtifactList(); + for (Map.Entry art : getArtifactManager()) { + r.addArtifact(art.getKey(), art.getValue()); + if (--n == 0) { + break; + } + } + r.collapseNodes(); + r.computeDisplayName(); + return r; } /** @@ -1067,11 +1077,50 @@ public abstract class Run ,RunT extends Run tree = new LinkedHashMap(); + private final Map directoryNodes = new HashMap(); + private int idSeq = 0; public Map getTree() { return tree; } + void addArtifact(String path, long length) { + StringBuilder href = new StringBuilder(); + String parentId = null; + int from = 0; + int to; + while (true) { + to = path.indexOf('/', from); + boolean end = to == -1; + String segment = end ? path.substring(from) : path.substring(from, to); + if (from > 0) { + href.append('/'); + } + href.append(Util.rawEncode(segment)); + if (end) { + break; + } else { + String d = path.substring(0, to); + Artifact p = directoryNodes.get(d); + if (p == null) { + String newParentId = "n" + ++idSeq; + p = new Artifact(segment, d, null, "", newParentId); + directoryNodes.put(d, p); + tree.put(p, parentId); + } + parentId = p.treeNodeId; + from = to + 1; + } + } + Artifact a = new Artifact(path.replaceFirst("^.+/", ""), path, href.toString(), Long.toString(length), "n" + ++idSeq); + add(a); + tree.put(a, parentId); + } + + void collapseNodes() { + // TODO + } + public void computeDisplayName() { if(size()>LIST_CUTOFF) return; // we are not going to display file names, so no point in computing this @@ -1146,7 +1195,7 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run> { /** * Called when this manager is loaded from disk. @@ -76,35 +73,17 @@ public abstract class ArtifactManager { * @throws IOException if deletion could not be completed * @throws InterruptedException if deletion was interrupted */ - public abstract boolean deleteArtifacts() throws IOException, InterruptedException; + public abstract boolean delete() throws IOException, InterruptedException; /** - * Returns an object that gets bound to URL under "job/JOB/15/artifact". - * - *

- * This object should displays all artifacts of a build in a web display. - * (The caller is responsible for checking {@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. - * - * @return - * the stapler-bound object to render the page. - * @throws HttpResponse - * if the implementation wants to redirect to elsewhere, throw any exception that implements - * {@link HttpResponse} to have Jenkins return a redirect (or any other HTTP request.) - * See {@link HttpResponses#redirectTo(String)} for example. + * Lists all artifacts currently archived. + * Does now throw {@link IOException} since implementations are encouraged to be lazy: + * in case there is a large number of artifacts, the caller may not actually ask about all of them. + * Only files should be returned, not directories. + * No particular order is required, though it is recommended to sort by {@link String#CASE_INSENSITIVE_ORDER} within a directory. + * @return a map from artifact relative path (as in {@link #load}) to file length */ - public abstract Object browseArtifacts(); - - /** - * Loads a manifest of some or all artifact records. - * @param n a maximum number of items to return - * @return a list of artifact records - */ - @SuppressWarnings("rawtypes") // managers can apply to different kinds of builds, so this is not feasible to type check - public abstract Run.ArtifactList getArtifactsUpTo(int n); + @Override public abstract Iterator> iterator(); /** * Load the contents of an artifact as a stream. @@ -114,6 +93,6 @@ public abstract class ArtifactManager { * @throws FileNotFoundException if no such artifact exists in this build * @throws IOException in case of some other problem */ - public abstract InputStream loadArtifact(String artifact) throws IOException; + public abstract InputStream load(String artifact) throws IOException; } diff --git a/core/src/main/java/jenkins/model/StandardArtifactManager.java b/core/src/main/java/jenkins/model/StandardArtifactManager.java index ae46b5f5cb..430753f9d4 100644 --- a/core/src/main/java/jenkins/model/StandardArtifactManager.java +++ b/core/src/main/java/jenkins/model/StandardArtifactManager.java @@ -24,21 +24,22 @@ package jenkins.model; +import com.google.common.collect.AbstractIterator; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.model.BuildListener; -import hudson.model.DirectoryBrowserSupport; import hudson.model.Run; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; -import java.util.concurrent.atomic.AtomicInteger; - -import hudson.util.HttpResponses; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; import java.util.Map; +import java.util.Queue; /** * Default artifact manager which transfers files over the remoting channel and stores them inside the build directory. @@ -63,7 +64,7 @@ public class StandardArtifactManager extends ArtifactManager { workspace.copyRecursiveTo(new FilePath.ExplicitlySpecifiedDirScanner(artifacts), new FilePath(dir), description); } - @Override public boolean deleteArtifacts() throws IOException, InterruptedException { + @Override public final boolean delete() throws IOException, InterruptedException { File ad = getArtifactsDir(); if (!ad.exists()) { return false; @@ -72,59 +73,35 @@ public class StandardArtifactManager extends ArtifactManager { return true; } - @Override public Object browseArtifacts() { - throw HttpResponses._throw(new DirectoryBrowserSupport(build, new FilePath(getArtifactsDir()), - build.getParent().getDisplayName() + ' ' + build.getDisplayName(), "package.png", true)); - } - - @SuppressWarnings("rawtypes") // super told me to - @Override public Run.ArtifactList getArtifactsUpTo(int n) { - Run.ArtifactList r = build.new ArtifactList(); - addArtifacts(build, getArtifactsDir(), "", "", r, null, n, new AtomicInteger()); - r.computeDisplayName(); - return r; - } - - @SuppressWarnings({"rawtypes", "unchecked"}) // trying to type-check this mess just seems hopeless - private static int addArtifacts(Run build, File dir, String path, String pathHref, Run.ArtifactList r, Run.Artifact parent, int upTo, AtomicInteger idSeq) { - 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); - Run.Artifact a; - if (collapsed) { - // Collapse single items into parent node where possible: - a = build.new Artifact(parent.getFileName() + '/' + child, childPath, - sub.isDirectory() ? null : childHref, length, - parent.getTreeNodeId()); - r.getTree().put(a, r.getTree().remove(parent)); - } else { - // Use null href for a directory: - a = build.new Artifact(child, childPath, - sub.isDirectory() ? null : childHref, length, - "n" + idSeq.incrementAndGet()); - r.getTree().put(a, parent!=null ? parent.getTreeNodeId() : null); + @Override public final Iterator> iterator() { + final File base = getArtifactsDir(); + return new AbstractIterator>() { + Queue paths = new LinkedList(Collections.singleton("/")); + @Override protected Map.Entry computeNext() { + while (true) { + String path = paths.poll(); + if (path == null) { + return endOfData(); + } + File f = new File(base, path); + if (f.isDirectory()) { + String[] kids = f.list(); + if (kids != null) { + Arrays.sort(kids, String.CASE_INSENSITIVE_ORDER); + for (String kid : kids) { + paths.add(path + '/' + kid); + } + } + } else { + // TODO Java 6: AbstractMap.SimpleImmutableEntry + return Collections.singletonMap(path, f.length()).entrySet().iterator().next(); + } + } } - if (sub.isDirectory()) { - n += addArtifacts(build, sub, childPath + '/', childHref + '/', r, a, upTo-n, idSeq); - if (n>=upTo) break; - } else { - // Don't store collapsed path in ArrayList (for correct data in external API) - r.add(collapsed ? build.new Artifact(child, a.relativePath, a.getHref(), length, a.getTreeNodeId()) : a); - if (++n>=upTo) break; - } - } - return n; + }; } - @Override public InputStream loadArtifact(String artifact) throws IOException { + @Override public final InputStream load(String artifact) throws IOException { return new FileInputStream(new File(getArtifactsDir(), artifact)); } diff --git a/core/src/test/java/hudson/model/RunTest.java b/core/src/test/java/hudson/model/RunTest.java index 3c14875adc..de3d670729 100644 --- a/core/src/test/java/hudson/model/RunTest.java +++ b/core/src/test/java/hudson/model/RunTest.java @@ -25,11 +25,13 @@ package hudson.model; import hudson.Util; +import hudson.model.Run.Artifact; import hudson.util.StreamTaskListener; import java.io.File; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.TimeZone; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -145,4 +147,38 @@ public class RunTest { assertEquals(a.get(1).getDisplayPath(), "a/a.xml"); } + @Test public void addArtifact() { + Run.ArtifactList list = new Run(new StubJob(), 0) {}.new ArtifactList(); + assertEquals("[]", showTree(list)); + list.addArtifact("README", 0); + assertEquals("[README]; README[n1]→null", showTree(list)); + list.addArtifact("dist/bin/x", 0); + list.addArtifact("dist/bin/Mac OS X/x.app", 0); + list.addArtifact("dist/doc/man/x.1", 0); + list.addArtifact("dist/doc/man/x.8", 0); + assertEquals("[README, dist/bin/x, dist/bin/Mac OS X/x.app, dist/doc/man/x.1, dist/doc/man/x.8]; " + + "README[n1]→null; " + + "dist[n2]→null; " + + "dist/bin[n3]→n2; " + + "dist/bin/x[n4]→n3; " + + "dist/bin/Mac OS X[n5]→n3; " + + "dist/bin/Mac%20OS%20X/x.app[n6]→n5; " + + "dist/doc[n7]→n2; " + + "dist/doc/man[n8]→n7; " + + "dist/doc/man/x.1[n9]→n8; " + + "dist/doc/man/x.8[n10]→n8", showTree(list)); + // TODO collapseNodes (doc/man) + } + private static String showTree(Run.ArtifactList list) { + StringBuilder b = new StringBuilder(); + b.append(list); + Map tree = list.getTree(); + for (Map.Entry entry : tree.entrySet()) { + b.append("; "); + Artifact a = entry.getKey(); + b.append(a.getHref() != null ? a.getHref() : a.relativePath).append('[').append(a.getTreeNodeId()).append(']').append('→').append(entry.getValue()); + } + return b.toString(); + } + } diff --git a/maven-plugin/src/main/java/hudson/maven/reporters/MavenArtifact.java b/maven-plugin/src/main/java/hudson/maven/reporters/MavenArtifact.java index bf24e2acc2..41284ef2b4 100755 --- a/maven-plugin/src/main/java/hudson/maven/reporters/MavenArtifact.java +++ b/maven-plugin/src/main/java/hudson/maven/reporters/MavenArtifact.java @@ -207,7 +207,7 @@ public final class MavenArtifact implements Serializable { f.deleteOnExit(); OutputStream os = new FileOutputStream(f); try { - Util.copyStreamAndClose(build.getArtifactManager().loadArtifact(artifactPath()), os); + Util.copyStreamAndClose(build.getArtifactManager().load(artifactPath()), os); } finally { os.close(); } @@ -223,7 +223,7 @@ public final class MavenArtifact implements Serializable { 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().loadArtifact(artifactPath()), rsp.getCompressedOutputStream(req)); + Util.copyStreamAndClose(parent.parent.getArtifactManager().load(artifactPath()), rsp.getCompressedOutputStream(req)); } }; } diff --git a/test/src/test/java/hudson/maven/MavenMultiModuleTest.java b/test/src/test/java/hudson/maven/MavenMultiModuleTest.java index c979dd7409..0b4e260a11 100644 --- a/test/src/test/java/hudson/maven/MavenMultiModuleTest.java +++ b/test/src/test/java/hudson/maven/MavenMultiModuleTest.java @@ -542,7 +542,7 @@ public class MavenMultiModuleTest { } archivings.put(name, m); } - @Override public boolean deleteArtifacts() throws IOException, InterruptedException { + @Override public boolean delete() throws IOException, InterruptedException { throw new IOException(); } @Override public Object browseArtifacts() { @@ -551,7 +551,7 @@ public class MavenMultiModuleTest { @Override public Run.ArtifactList getArtifactsUpTo(int n) { throw new UnsupportedOperationException(); } - @Override public InputStream loadArtifact(String artifact) throws IOException { + @Override public InputStream load(String artifact) throws IOException { throw new IOException(); } }