PageRenderTime 89ms CodeModel.GetById 54ms app.highlight 28ms RepoModel.GetById 2ms app.codeStats 0ms

/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
  1/*
  2 * The MIT License
  3 * 
  4 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt
  5 * 
  6 * Permission is hereby granted, free of charge, to any person obtaining a copy
  7 * of this software and associated documentation files (the "Software"), to deal
  8 * in the Software without restriction, including without limitation the rights
  9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10 * copies of the Software, and to permit persons to whom the Software is
 11 * furnished to do so, subject to the following conditions:
 12 * 
 13 * The above copyright notice and this permission notice shall be included in
 14 * all copies or substantial portions of the Software.
 15 * 
 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22 * THE SOFTWARE.
 23 */
 24package hudson.model;
 25
 26import hudson.FilePath;
 27import hudson.Util;
 28import hudson.util.IOException2;
 29import hudson.FilePath.FileCallable;
 30import hudson.remoting.VirtualChannel;
 31import org.kohsuke.stapler.StaplerRequest;
 32import org.kohsuke.stapler.StaplerResponse;
 33import org.kohsuke.stapler.HttpResponse;
 34import org.apache.tools.ant.types.FileSet;
 35import org.apache.tools.ant.DirectoryScanner;
 36
 37import javax.servlet.ServletException;
 38import javax.servlet.http.HttpServletResponse;
 39import java.io.File;
 40import java.io.FilenameFilter;
 41import java.io.IOException;
 42import java.io.InputStream;
 43import java.io.OutputStream;
 44import java.io.Serializable;
 45import java.util.ArrayList;
 46import java.util.Arrays;
 47import java.util.Collections;
 48import java.util.Comparator;
 49import java.util.List;
 50import java.util.StringTokenizer;
 51import java.util.logging.Logger;
 52import java.util.logging.Level;
 53
 54/**
 55 * Has convenience methods to serve file system.
 56 *
 57 * <p>
 58 * This object can be used in a mix-in style to provide a directory browsing capability
 59 * to a {@link ModelObject}. 
 60 *
 61 * @author Kohsuke Kawaguchi
 62 */
 63public final class DirectoryBrowserSupport implements HttpResponse {
 64
 65    //TODO: review and check whether we can do it private
 66    public final ModelObject owner;
 67    //TODO: review and check whether we can do it private
 68    public final String title;
 69
 70    private final FilePath base;
 71    private final String icon;
 72    private final boolean serveDirIndex;
 73    private String indexFileName = "index.html";
 74    /**
 75     * @deprecated as of 1.297
 76     *      Use {@link #DirectoryBrowserSupport(ModelObject, FilePath, String, String, boolean)}
 77     */
 78    public DirectoryBrowserSupport(ModelObject owner, String title) {
 79        this(owner,null,title,null,false);
 80    }
 81
 82    /**
 83     * @param owner
 84     *      The parent model object under which the directory browsing is added.
 85     * @param base
 86     *      The root of the directory that's bound to URL.
 87     * @param title
 88     *      Used in the HTML caption.
 89     * @param icon
 90     *      The icon file name, like "folder.gif"
 91     * @param serveDirIndex
 92     *      True to generate the directory index.
 93     *      False to serve "index.html"
 94     */
 95    public DirectoryBrowserSupport(ModelObject owner, FilePath base, String title, String icon, boolean serveDirIndex) {
 96        this.owner = owner;
 97        this.base = base;
 98        this.title = title;
 99        this.icon = icon;
100        this.serveDirIndex = serveDirIndex;
101    }
102
103    public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
104        try {
105            serveFile(req,rsp,base,icon,serveDirIndex);
106        } catch (InterruptedException e) {
107            throw new IOException2("interrupted",e);
108        }
109    }
110
111    /**
112     * If the directory is requested but the directory listing is disabled, a file of this name
113     * is served. By default it's "index.html".
114     * @since 1.312
115     */
116    public void setIndexFileName(String fileName) {
117        this.indexFileName = fileName;
118    }
119
120    /**
121     * Serves a file from the file system (Maps the URL to a directory in a file system.)
122     *
123     * @param icon
124     *      The icon file name, like "folder-open.gif"
125     * @param serveDirIndex
126     *      True to generate the directory index.
127     *      False to serve "index.html"
128     * @deprecated as of 1.297
129     *      Instead of calling this method explicitly, just return the {@link DirectoryBrowserSupport} object
130     *      from the {@code doXYZ} method and let Stapler generate a response for you.
131     */
132    public void serveFile(StaplerRequest req, StaplerResponse rsp, FilePath root, String icon, boolean serveDirIndex) throws IOException, ServletException, InterruptedException {
133        // handle form submission
134        String pattern = req.getParameter("pattern");
135        if(pattern==null)
136            pattern = req.getParameter("path"); // compatibility with Hudson<1.129
137        if(pattern!=null) {
138            rsp.sendRedirect2(pattern);
139            return;
140        }
141
142        String path = getPath(req);
143        if(path.replace('\\','/').indexOf("/../")!=-1) {
144            // don't serve anything other than files in the artifacts dir
145            rsp.sendError(HttpServletResponse.SC_BAD_REQUEST);
146            return;
147        }
148
149        // split the path to the base directory portion "abc/def/ghi" which doesn't include any wildcard,
150        // and the GLOB portion "**/*.xml" (the rest)
151        StringBuilder _base = new StringBuilder();
152        StringBuilder _rest = new StringBuilder();
153        int restSize=-1; // number of ".." needed to go back to the 'base' level.
154        boolean zip=false;  // if we are asked to serve a zip file bundle
155        boolean plain = false; // if asked to serve a plain text directory listing
156        {
157            boolean inBase = true;
158            StringTokenizer pathTokens = new StringTokenizer(path,"/");
159            while(pathTokens.hasMoreTokens()) {
160                String pathElement = pathTokens.nextToken();
161                // Treat * and ? as wildcard unless they match a literal filename
162                if((pathElement.contains("?") || pathElement.contains("*"))
163                        && inBase && !(new FilePath(root, (_base.length() > 0 ? _base + "/" : "") + pathElement).exists()))
164                    inBase = false;
165                if(pathElement.equals("*zip*")) {
166                    // the expected syntax is foo/bar/*zip*/bar.zip
167                    // the last 'bar.zip' portion is to causes browses to set a good default file name.
168                    // so the 'rest' portion ends here.
169                    zip=true;
170                    break;
171                }
172                if(pathElement.equals("*plain*")) {
173                    plain = true;
174                    break;
175                }
176
177                StringBuilder sb = inBase?_base:_rest;
178                if(sb.length()>0)   sb.append('/');
179                sb.append(pathElement);
180                if(!inBase)
181                    restSize++;
182            }
183        }
184        restSize = Math.max(restSize,0);
185        String base = _base.toString();
186        String rest = _rest.toString();
187
188        // this is the base file/directory
189        FilePath baseFile = new FilePath(root,base);
190
191        if(baseFile.isDirectory()) {
192            if(zip) {
193                rsp.setContentType("application/zip");
194                baseFile.zip(rsp.getOutputStream(),rest);
195                return;
196            }
197            if (plain) {
198                rsp.setContentType("text/plain;charset=UTF-8");
199                OutputStream os = rsp.getOutputStream();
200                try {
201                    for (String kid : baseFile.act(new SimpleChildList())) {
202                        os.write(kid.getBytes("UTF-8"));
203                        os.write('\n');
204                    }
205                    os.flush();
206                } finally {
207                    os.close();
208                }
209                return;
210            }
211
212            if(rest.length()==0) {
213                // if the target page to be displayed is a directory and the path doesn't end with '/', redirect
214                StringBuffer reqUrl = req.getRequestURL();
215                if(reqUrl.charAt(reqUrl.length()-1)!='/') {
216                    rsp.sendRedirect2(reqUrl.append('/').toString());
217                    return;
218                }
219            }
220
221            FileCallable<List<List<Path>>> glob = null;
222
223            if(rest.length()>0) {
224                // the rest is Ant glob pattern
225                glob = new PatternScanner(rest,createBackRef(restSize));
226            } else
227            if(serveDirIndex) {
228                // serve directory index
229                glob = new ChildPathBuilder();
230            }
231
232            if(glob!=null) {
233                // serve glob
234                req.setAttribute("it", this);
235                List<Path> parentPaths = buildParentPath(base,restSize);
236                req.setAttribute("parentPath",parentPaths);
237                req.setAttribute("backPath", createBackRef(restSize));
238                req.setAttribute("topPath", createBackRef(parentPaths.size()+restSize));
239                req.setAttribute("files", baseFile.act(glob));
240                req.setAttribute("icon", icon);
241                req.setAttribute("path", path);
242                req.setAttribute("pattern",rest);
243                req.setAttribute("dir", baseFile);
244                req.getView(this,"dir.jelly").forward(req, rsp);
245                return;
246            }
247
248            // convert a directory service request to a single file service request by serving
249            // 'index.html'
250            baseFile = baseFile.child(indexFileName);
251        }
252
253        //serve a single file
254        if(!baseFile.exists()) {
255            rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
256            return;
257        }
258
259        boolean view = rest.equals("*view*");
260
261        if(rest.equals("*fingerprint*")) {
262            rsp.forward(Hudson.getInstance().getFingerprint(baseFile.digest()),"/",req);
263            return;
264        }
265
266        ContentInfo ci = baseFile.act(new ContentInfo());
267
268        if(LOGGER.isLoggable(Level.FINE))
269            LOGGER.fine("Serving "+baseFile+" with lastModified="+ci.lastModified+", contentLength="+ci.contentLength);
270
271        InputStream in = baseFile.read();
272        if (view) {
273            // for binary files, provide the file name for download
274            rsp.setHeader("Content-Disposition", "inline; filename=" + baseFile.getName());
275
276            // pseudo file name to let the Stapler set text/plain
277            rsp.serveFile(req, in, ci.lastModified, -1, ci.contentLength, "plain.txt");
278        } else {
279            rsp.serveFile(req, in, ci.lastModified, -1, ci.contentLength, baseFile.getName() );
280        }
281    }
282
283    private String getPath(StaplerRequest req) {
284        String path = req.getRestOfPath();
285        if(path.length()==0)
286            path = "/";
287        return path;
288    }
289
290    public ModelObject getOwner() {
291        return owner;
292    }
293
294    public String getTitle() {
295        return title;
296    }
297
298    private static final class ContentInfo implements FileCallable<ContentInfo> {
299        long contentLength;
300        long lastModified;
301
302        public ContentInfo invoke(File f, VirtualChannel channel) throws IOException {
303            contentLength = f.length();
304            lastModified = f.lastModified();
305            return this;
306        }
307
308        private static final long serialVersionUID = 1L;
309    }
310
311    /**
312     * Builds a list of {@link Path} that represents ancestors
313     * from a string like "/foo/bar/zot".
314     */
315    private List<Path> buildParentPath(String pathList, int restSize) {
316        List<Path> r = new ArrayList<Path>();
317        StringTokenizer tokens = new StringTokenizer(pathList, "/");
318        int total = tokens.countTokens();
319        int current=1;
320        while(tokens.hasMoreTokens()) {
321            String token = tokens.nextToken();
322            r.add(new Path(createBackRef(total-current+restSize),token,true,0, true));
323            current++;
324        }
325        return r;
326    }
327
328    private static String createBackRef(int times) {
329        if(times==0)    return "./";
330        StringBuilder buf = new StringBuilder(3*times);
331        for(int i=0; i<times; i++ )
332            buf.append("../");
333        return buf.toString();
334    }
335
336    /**
337     * Represents information about one file or folder.
338     */
339    public static final class Path implements Serializable {
340        /**
341         * Relative URL to this path from the current page.
342         */
343        private final String href;
344        /**
345         * Name of this path. Just the file name portion.
346         */
347        private final String title;
348
349        private final boolean isFolder;
350
351        /**
352         * File size, or null if this is not a file.
353         */
354        private final long size;
355        
356        /**
357         * If the current user can read the file. 
358         */
359        private final boolean isReadable;
360
361        public Path(String href, String title, boolean isFolder, long size, boolean isReadable) {
362            this.href = href;
363            this.title = title;
364            this.isFolder = isFolder;
365            this.size = size;
366            this.isReadable = isReadable;
367        }
368
369        public boolean isFolder() {
370            return isFolder;
371        }
372        
373        public boolean isReadable() {
374            return isReadable;
375        }
376
377        public String getHref() {
378            return href;
379        }
380
381        public String getTitle() {
382            return title;
383        }
384
385        public String getIconName() {
386            if (isReadable)
387                return isFolder?"folder.gif":"text.gif";
388            else
389                return isFolder?"folder-error.gif":"text-error.gif";
390        }
391
392        public long getSize() {
393            return size;
394        }
395
396        private static final long serialVersionUID = 1L;
397    }
398
399
400
401    private static final class FileComparator implements Comparator<File> {
402        public int compare(File lhs, File rhs) {
403            // directories first, files next
404            int r = dirRank(lhs)-dirRank(rhs);
405            if(r!=0) return r;
406            // otherwise alphabetical
407            return lhs.getName().compareTo(rhs.getName());
408        }
409
410        private int dirRank(File f) {
411            if(f.isDirectory())     return 0;
412            else                    return 1;
413        }
414    }
415
416    /**
417     * Simple list of names of children of a folder.
418     * Subfolders will have a trailing slash appended.
419     */
420    private static final class SimpleChildList implements FileCallable<List<String>> {
421        private static final long serialVersionUID = 1L;
422        public List<String> invoke(File f, VirtualChannel channel) throws IOException {
423            List<String> r = new ArrayList<String>();
424            String[] kids = f.list(); // no need to sort
425            for (String kid : kids) {
426                if (new File(f, kid).isDirectory()) {
427                    r.add(kid + "/");
428                } else {
429                    r.add(kid);
430                }
431            }
432            return r;
433        }
434    }
435
436    /**
437     * Builds a list of list of {@link Path}. The inner
438     * list of {@link Path} represents one child item to be shown
439     * (this mechanism is used to skip empty intermediate directory.)
440     */
441    private static final class ChildPathBuilder implements FileCallable<List<List<Path>>> {
442        public List<List<Path>> invoke(File cur, VirtualChannel channel) throws IOException {
443            List<List<Path>> r = new ArrayList<List<Path>>();
444
445            File[] files = cur.listFiles();
446            if (files != null) {
447                Arrays.sort(files,new FileComparator());
448    
449                for( File f : files ) {
450                    Path p = new Path(Util.rawEncode(f.getName()),f.getName(),f.isDirectory(),f.length(), f.canRead());
451                    if(!f.isDirectory()) {
452                        r.add(Collections.singletonList(p));
453                    } else {
454                        // find all empty intermediate directory
455                        List<Path> l = new ArrayList<Path>();
456                        l.add(p);
457                        String relPath = Util.rawEncode(f.getName());
458                        while(true) {
459                            // files that don't start with '.' qualify for 'meaningful files', nor SCM related files
460                            File[] sub = f.listFiles(new FilenameFilter() {
461                                public boolean accept(File dir, String name) {
462                                    return !name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn");
463                                }
464                            });
465                            if(sub==null || sub.length!=1 || !sub[0].isDirectory())
466                                break;
467                            f = sub[0];
468                            relPath += '/'+Util.rawEncode(f.getName());
469                            l.add(new Path(relPath,f.getName(),true,0, f.canRead()));
470                        }
471                        r.add(l);
472                    }
473                }
474            }
475
476            return r;
477        }
478
479        private static final long serialVersionUID = 1L;
480    }
481
482    /**
483     * Runs ant GLOB against the current {@link FilePath} and returns matching
484     * paths.
485     */
486    private static class PatternScanner implements FileCallable<List<List<Path>>> {
487        private final String pattern;
488        /**
489         * String like "../../../" that cancels the 'rest' portion. Can be "./"
490         */
491        private final String baseRef;
492
493        public PatternScanner(String pattern,String baseRef) {
494            this.pattern = pattern;
495            this.baseRef = baseRef;
496        }
497
498        public List<List<Path>> invoke(File baseDir, VirtualChannel channel) throws IOException {
499            FileSet fs = Util.createFileSet(baseDir,pattern);
500            DirectoryScanner ds = fs.getDirectoryScanner();
501            String[] files = ds.getIncludedFiles();
502
503            if (files.length > 0) {
504                List<List<Path>> r = new ArrayList<List<Path>>(files.length);
505                for (String match : files) {
506                    List<Path> file = buildPathList(baseDir, new File(baseDir,match));
507                    r.add(file);
508                }
509                return r;
510            }
511
512            return null;
513        }
514
515        /**
516         * Builds a path list from the current workspace directory down to the specified file path.
517         */
518        private List<Path> buildPathList(File baseDir, File filePath) throws IOException {
519            List<Path> pathList = new ArrayList<Path>();
520            StringBuilder href = new StringBuilder(baseRef);
521
522            buildPathList(baseDir, filePath, pathList, href);
523            return pathList;
524        }
525
526        /**
527         * Builds the path list and href recursively top-down.
528         */
529        private void buildPathList(File baseDir, File filePath, List<Path> pathList, StringBuilder href) throws IOException {
530            File parent = filePath.getParentFile();
531            if (!baseDir.equals(parent)) {
532                buildPathList(baseDir, parent, pathList, href);
533            }
534
535            href.append(Util.rawEncode(filePath.getName()));
536            if (filePath.isDirectory()) {
537                href.append("/");
538            }
539
540            Path path = new Path(href.toString(), filePath.getName(), filePath.isDirectory(), filePath.length(), filePath.canRead());
541            pathList.add(path);
542        }
543
544        private static final long serialVersionUID = 1L;
545    }
546
547    private static final Logger LOGGER = Logger.getLogger(DirectoryBrowserSupport.class.getName());
548}