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