/hudson-core/src/main/java/hudson/model/DirectoryBrowserSupport.java
http://github.com/hudson/hudson · Java · 548 lines · 349 code · 68 blank · 131 comment · 49 complexity · 6d192caa5502b22afdff0156d5c8f459 MD5 · raw file
- /*
- * The MIT License
- *
- * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt
- *
- * 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 hudson.model;
- import hudson.FilePath;
- import hudson.Util;
- import hudson.util.IOException2;
- import hudson.FilePath.FileCallable;
- import hudson.remoting.VirtualChannel;
- 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;
- import java.io.Serializable;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collections;
- import java.util.Comparator;
- import java.util.List;
- import java.util.StringTokenizer;
- import java.util.logging.Logger;
- import java.util.logging.Level;
- /**
- * Has convenience methods to serve file system.
- *
- * <p>
- * This object can be used in a mix-in style to provide a directory browsing capability
- * to a {@link ModelObject}.
- *
- * @author Kohsuke Kawaguchi
- */
- public final class DirectoryBrowserSupport implements HttpResponse {
- //TODO: review and check whether we can do it private
- public final ModelObject owner;
- //TODO: review and check whether we can do it private
- public final String title;
- private final FilePath base;
- private final String icon;
- private final boolean serveDirIndex;
- private String indexFileName = "index.html";
- /**
- * @deprecated as of 1.297
- * Use {@link #DirectoryBrowserSupport(ModelObject, FilePath, String, String, boolean)}
- */
- public DirectoryBrowserSupport(ModelObject owner, String title) {
- this(owner,null,title,null,false);
- }
- /**
- * @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"
- */
- public DirectoryBrowserSupport(ModelObject owner, FilePath base, String title, String icon, boolean serveDirIndex) {
- this.owner = owner;
- this.base = base;
- this.title = title;
- this.icon = icon;
- this.serveDirIndex = serveDirIndex;
- }
- public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
- try {
- serveFile(req,rsp,base,icon,serveDirIndex);
- } catch (InterruptedException e) {
- throw new IOException2("interrupted",e);
- }
- }
- /**
- * If the directory is requested but the directory listing is disabled, a file of this name
- * is served. By default it's "index.html".
- * @since 1.312
- */
- public void setIndexFileName(String fileName) {
- this.indexFileName = fileName;
- }
- /**
- * Serves a file from the file system (Maps the URL to a directory in a file system.)
- *
- * @param icon
- * The icon file name, like "folder-open.gif"
- * @param serveDirIndex
- * True to generate the directory index.
- * False to serve "index.html"
- * @deprecated as of 1.297
- * Instead of calling this method explicitly, just return the {@link DirectoryBrowserSupport} object
- * 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 {
- // handle form submission
- String pattern = req.getParameter("pattern");
- if(pattern==null)
- pattern = req.getParameter("path"); // compatibility with Hudson<1.129
- if(pattern!=null) {
- rsp.sendRedirect2(pattern);
- return;
- }
- String path = getPath(req);
- if(path.replace('\\','/').indexOf("/../")!=-1) {
- // don't serve anything other than files in the artifacts dir
- rsp.sendError(HttpServletResponse.SC_BAD_REQUEST);
- return;
- }
- // split the path to the base directory portion "abc/def/ghi" which doesn't include any wildcard,
- // and the GLOB portion "**/*.xml" (the rest)
- StringBuilder _base = new StringBuilder();
- StringBuilder _rest = new StringBuilder();
- int restSize=-1; // number of ".." needed to go back to the 'base' level.
- boolean zip=false; // if we are asked to serve a zip file bundle
- boolean plain = false; // if asked to serve a plain text directory listing
- {
- boolean inBase = true;
- StringTokenizer pathTokens = new StringTokenizer(path,"/");
- while(pathTokens.hasMoreTokens()) {
- 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 = false;
- if(pathElement.equals("*zip*")) {
- // the expected syntax is foo/bar/*zip*/bar.zip
- // the last 'bar.zip' portion is to causes browses to set a good default file name.
- // so the 'rest' portion ends here.
- zip=true;
- break;
- }
- if(pathElement.equals("*plain*")) {
- plain = true;
- break;
- }
- StringBuilder sb = inBase?_base:_rest;
- if(sb.length()>0) sb.append('/');
- sb.append(pathElement);
- if(!inBase)
- restSize++;
- }
- }
- restSize = Math.max(restSize,0);
- String base = _base.toString();
- String rest = _rest.toString();
- // this is the base file/directory
- FilePath baseFile = new FilePath(root,base);
- if(baseFile.isDirectory()) {
- if(zip) {
- rsp.setContentType("application/zip");
- baseFile.zip(rsp.getOutputStream(),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"));
- os.write('\n');
- }
- os.flush();
- } finally {
- os.close();
- }
- return;
- }
- if(rest.length()==0) {
- // if the target page to be displayed is a directory and the path doesn't end with '/', redirect
- StringBuffer reqUrl = req.getRequestURL();
- if(reqUrl.charAt(reqUrl.length()-1)!='/') {
- rsp.sendRedirect2(reqUrl.append('/').toString());
- return;
- }
- }
- FileCallable<List<List<Path>>> glob = null;
- if(rest.length()>0) {
- // the rest is Ant glob pattern
- glob = new PatternScanner(rest,createBackRef(restSize));
- } else
- if(serveDirIndex) {
- // serve directory index
- glob = new ChildPathBuilder();
- }
- if(glob!=null) {
- // serve glob
- req.setAttribute("it", this);
- List<Path> parentPaths = buildParentPath(base,restSize);
- req.setAttribute("parentPath",parentPaths);
- req.setAttribute("backPath", createBackRef(restSize));
- req.setAttribute("topPath", createBackRef(parentPaths.size()+restSize));
- req.setAttribute("files", baseFile.act(glob));
- req.setAttribute("icon", icon);
- req.setAttribute("path", path);
- req.setAttribute("pattern",rest);
- req.setAttribute("dir", baseFile);
- req.getView(this,"dir.jelly").forward(req, rsp);
- return;
- }
- // convert a directory service request to a single file service request by serving
- // 'index.html'
- baseFile = baseFile.child(indexFileName);
- }
- //serve a single file
- if(!baseFile.exists()) {
- rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
- return;
- }
- boolean view = rest.equals("*view*");
- if(rest.equals("*fingerprint*")) {
- rsp.forward(Hudson.getInstance().getFingerprint(baseFile.digest()),"/",req);
- return;
- }
- ContentInfo ci = baseFile.act(new ContentInfo());
- if(LOGGER.isLoggable(Level.FINE))
- LOGGER.fine("Serving "+baseFile+" with lastModified="+ci.lastModified+", contentLength="+ci.contentLength);
- InputStream in = baseFile.read();
- 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");
- } else {
- rsp.serveFile(req, in, ci.lastModified, -1, ci.contentLength, baseFile.getName() );
- }
- }
- private String getPath(StaplerRequest req) {
- String path = req.getRestOfPath();
- if(path.length()==0)
- path = "/";
- return path;
- }
- public ModelObject getOwner() {
- return owner;
- }
- public String getTitle() {
- return title;
- }
- 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".
- */
- private List<Path> buildParentPath(String pathList, int restSize) {
- List<Path> r = new ArrayList<Path>();
- StringTokenizer tokens = new StringTokenizer(pathList, "/");
- int total = tokens.countTokens();
- int current=1;
- while(tokens.hasMoreTokens()) {
- String token = tokens.nextToken();
- r.add(new Path(createBackRef(total-current+restSize),token,true,0, true));
- current++;
- }
- return r;
- }
- private static String createBackRef(int times) {
- if(times==0) return "./";
- StringBuilder buf = new StringBuilder(3*times);
- for(int i=0; i<times; i++ )
- buf.append("../");
- return buf.toString();
- }
- /**
- * Represents information about one file or folder.
- */
- public static final class Path implements Serializable {
- /**
- * Relative URL to this path from the current page.
- */
- private final String href;
- /**
- * Name of this path. Just the file name portion.
- */
- private final String title;
- private final boolean isFolder;
- /**
- * File size, or null if this is not a file.
- */
- private final long size;
-
- /**
- * If the current user can read the file.
- */
- private final boolean isReadable;
- public Path(String href, String title, boolean isFolder, long size, boolean isReadable) {
- this.href = href;
- this.title = title;
- this.isFolder = isFolder;
- this.size = size;
- this.isReadable = isReadable;
- }
- public boolean isFolder() {
- return isFolder;
- }
-
- public boolean isReadable() {
- return isReadable;
- }
- public String getHref() {
- return href;
- }
- public String getTitle() {
- return title;
- }
- public String getIconName() {
- if (isReadable)
- return isFolder?"folder.gif":"text.gif";
- else
- return isFolder?"folder-error.gif":"text-error.gif";
- }
- public long getSize() {
- return size;
- }
- private static final long serialVersionUID = 1L;
- }
- private static final class FileComparator implements Comparator<File> {
- public int compare(File lhs, File rhs) {
- // directories first, files next
- int r = dirRank(lhs)-dirRank(rhs);
- if(r!=0) return r;
- // otherwise alphabetical
- return lhs.getName().compareTo(rhs.getName());
- }
- private int dirRank(File f) {
- 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);
- }
- }
- return r;
- }
- }
- /**
- * Builds a list of list of {@link Path}. The inner
- * 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>>> {
- public List<List<Path>> invoke(File cur, VirtualChannel channel) throws IOException {
- List<List<Path>> r = new ArrayList<List<Path>>();
- File[] files = cur.listFiles();
- if (files != null) {
- Arrays.sort(files,new FileComparator());
-
- for( File 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 {
- // find all empty intermediate directory
- List<Path> l = new ArrayList<Path>();
- l.add(p);
- 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");
- }
- });
- if(sub==null || sub.length!=1 || !sub[0].isDirectory())
- break;
- f = sub[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.
- */
- 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();
- 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));
- r.add(file);
- }
- return r;
- }
- return null;
- }
- /**
- * 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 {
- List<Path> pathList = new ArrayList<Path>();
- StringBuilder href = new StringBuilder(baseRef);
- buildPathList(baseDir, filePath, pathList, href);
- return pathList;
- }
- /**
- * 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();
- if (!baseDir.equals(parent)) {
- buildPathList(baseDir, parent, pathList, href);
- }
- href.append(Util.rawEncode(filePath.getName()));
- if (filePath.isDirectory()) {
- href.append("/");
- }
- Path path = new Path(href.toString(), filePath.getName(), filePath.isDirectory(), filePath.length(), filePath.canRead());
- pathList.add(path);
- }
- private static final long serialVersionUID = 1L;
- }
- private static final Logger LOGGER = Logger.getLogger(DirectoryBrowserSupport.class.getName());
- }