001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.microservice.resources; 014 015import static java.util.logging.Level.*; 016import static javax.servlet.http.HttpServletResponse.*; 017import static org.apache.juneau.html.HtmlDocSerializer.*; 018import static org.apache.juneau.http.HttpMethodName.*; 019 020import java.io.*; 021import java.net.*; 022import java.util.*; 023import java.util.logging.*; 024 025import javax.servlet.*; 026 027import org.apache.juneau.annotation.*; 028import org.apache.juneau.rest.*; 029import org.apache.juneau.rest.annotation.*; 030import org.apache.juneau.rest.converters.*; 031import org.apache.juneau.transforms.*; 032import org.apache.juneau.utils.*; 033 034/** 035 * REST resource that allows access to a file system directory. 036 * 037 * <p> 038 * The root directory is specified in one of two ways: 039 * <ul class='spaced-list'> 040 * <li> 041 * Specifying the location via a <l>DirectoryResource.rootDir</l> property. 042 * <li> 043 * Overriding the {@link #getRootDir()} method. 044 * </ul> 045 * 046 * <p> 047 * Read/write access control is handled through the following properties: 048 * <ul class='spaced-list'> 049 * <li> 050 * <l>DirectoryResource.allowViews</l> - If <jk>true</jk>, allows view and download access to files. 051 * <li> 052 * <l>DirectoryResource.allowPuts</l> - If <jk>true</jk>, allows files to be created or overwritten. 053 * <li> 054 * <l>DirectoryResource.allowDeletes</l> - If <jk>true</jk>, allows files to be deleted. 055 * </ul> 056 * 057 * <p> 058 * Access can also be controlled by overriding the {@link #checkAccess(RestRequest)} method. 059 */ 060@RestResource( 061 title="File System Explorer", 062 description="Contents of $RA{path}", 063 messages="nls/DirectoryResource", 064 htmldoc=@HtmlDoc( 065 navlinks={ 066 "up: request:/..", 067 "options: servlet:/?method=OPTIONS" 068 } 069 ), 070 allowedMethodParams="*", 071 properties={ 072 @Property(name=HTML_uriAnchorText, value="PROPERTY_NAME"), 073 @Property(name="DirectoryResource.rootDir", value="") 074 } 075) 076public class DirectoryResource extends BasicRestServlet { 077 private static final long serialVersionUID = 1L; 078 079 private File rootDir; // The root directory 080 081 // Settings enabled through servlet init parameters 082 boolean allowDeletes, allowPuts, allowViews; 083 084 private static Logger logger = Logger.getLogger(DirectoryResource.class.getName()); 085 086 @Override /* Servlet */ 087 public void init() throws ServletException { 088 RestContextProperties p = getProperties(); 089 rootDir = new File(p.getString("DirectoryResource.rootDir")); 090 allowViews = p.getBoolean("DirectoryResource.allowViews", false); 091 allowDeletes = p.getBoolean("DirectoryResource.allowDeletes", false); 092 allowPuts = p.getBoolean("DirectoryResource.allowPuts", false); 093 } 094 095 /** 096 * Returns the root directory defined by the 'rootDir' init parameter. 097 * 098 * <p> 099 * Subclasses can override this method to provide their own root directory. 100 * 101 * @return The root directory. 102 */ 103 protected File getRootDir() { 104 if (rootDir == null) { 105 rootDir = new File(getProperties().getString("rootDir")); 106 if (! rootDir.exists()) 107 if (! rootDir.mkdirs()) 108 throw new RuntimeException("Could not create root dir"); 109 } 110 return rootDir; 111 } 112 113 /** 114 * [GET /*] - On directories, returns a directory listing. On files, returns information about the file. 115 * 116 * @param req The HTTP request. 117 * @return Either a FileResource or list of FileResources depending on whether it's a 118 * file or directory. 119 * @throws Exception If file could not be read or access was not granted. 120 */ 121 @RestMethod(name=GET, path="/*", 122 description="On directories, returns a directory listing.\nOn files, returns information about the file.", 123 converters={Queryable.class} 124 ) 125 public Object doGet(RestRequest req) throws Exception { 126 checkAccess(req); 127 128 String pathInfo = req.getPathInfo(); 129 File f = pathInfo == null ? rootDir : new File(rootDir.getAbsolutePath() + pathInfo); 130 131 if (!f.exists()) 132 throw new RestException(SC_NOT_FOUND, "File not found"); 133 134 req.setAttribute("path", f.getAbsolutePath()); 135 136 if (f.isDirectory()) { 137 List<FileResource> l = new LinkedList<>(); 138 File[] files = f.listFiles(); 139 if (files != null) { 140 for (File fc : files) { 141 URL fUrl = new URL(req.getRequestURL().append("/").append(fc.getName()).toString()); 142 l.add(new FileResource(fc, fUrl)); 143 } 144 } 145 return l; 146 } 147 148 return new FileResource(f, new URL(req.getRequestURL().toString())); 149 } 150 151 /** 152 * [DELETE /*] - Delete a file on the file system. 153 * 154 * @param req The HTTP request. 155 * @return The message <js>"File deleted"</js> if successful. 156 * @throws Exception If file could not be read or access was not granted. 157 */ 158 @RestMethod(name=DELETE, path="/*", 159 description="Delete a file on the file system." 160 ) 161 public Object doDelete(RestRequest req) throws Exception { 162 checkAccess(req); 163 164 File f = new File(rootDir.getAbsolutePath() + req.getPathInfo()); 165 deleteFile(f); 166 167 if (req.getHeader("Accept").contains("text/html")) 168 return new Redirect(); 169 return "File deleted"; 170 } 171 172 /** 173 * [PUT /*] - Add or overwrite a file on the file system. 174 * 175 * @param req The HTTP request. 176 * @return The message <js>"File added"</js> if successful. 177 * @throws Exception If file could not be read or access was not granted. 178 */ 179 @RestMethod(name=PUT, path="/*", 180 description="Add or overwrite a file on the file system." 181 ) 182 public Object doPut(RestRequest req) throws Exception { 183 checkAccess(req); 184 185 File f = new File(rootDir.getAbsolutePath() + req.getPathInfo()); 186 String parentSubPath = f.getParentFile().getAbsolutePath().substring(rootDir.getAbsolutePath().length()); 187 try (InputStream is = req.getInputStream(); OutputStream os = new BufferedOutputStream(new FileOutputStream(f))) { 188 IOPipe.create(is, os).run(); 189 } 190 if (req.getContentType().contains("html")) 191 return new Redirect(parentSubPath); 192 return "File added"; 193 } 194 195 /** 196 * [VIEW /*] - View the contents of a file. 197 * 198 * <p> 199 * Applies to files only. 200 * 201 * @param req The HTTP request. 202 * @param res The HTTP response. 203 * @return A Reader containing the contents of the file. 204 * @throws Exception If file could not be read or access was not granted. 205 */ 206 @RestMethod(name="VIEW", path="/*", 207 description="View the contents of a file.\nApplies to files only." 208 ) 209 public Reader doView(RestRequest req, RestResponse res) throws Exception { 210 checkAccess(req); 211 212 File f = new File(rootDir.getAbsolutePath() + req.getPathInfo()); 213 214 if (!f.exists()) 215 throw new RestException(SC_NOT_FOUND, "File not found"); 216 217 if (f.isDirectory()) 218 throw new RestException(SC_METHOD_NOT_ALLOWED, "VIEW not available on directories"); 219 220 res.setContentType("text/plain"); 221 return new FileReader(f); 222 } 223 224 /** 225 * [DOWNLOAD /*] - Download the contents of a file. 226 * 227 * <p> 228 * Applies to files only. 229 * 230 * @param req The HTTP request. 231 * @param res The HTTP response. 232 * @return A Reader containing the contents of the file. 233 * @throws Exception If file could not be read or access was not granted. 234 */ 235 @RestMethod(name="DOWNLOAD", path="/*", 236 description="Download the contents of a file.\nApplies to files only." 237 ) 238 public Reader doDownload(RestRequest req, RestResponse res) throws Exception { 239 checkAccess(req); 240 241 File f = new File(rootDir.getAbsolutePath() + req.getPathInfo()); 242 243 if (!f.exists()) 244 throw new RestException(SC_NOT_FOUND, "File not found"); 245 246 if (f.isDirectory()) 247 throw new RestException(SC_METHOD_NOT_ALLOWED, "DOWNLOAD not available on directories"); 248 249 res.setContentType("application"); 250 return new FileReader(f); 251 } 252 253 /** 254 * Verify that the specified request is allowed. 255 * 256 * <p> 257 * Subclasses can override this method to provide customized behavior. 258 * Method should throw a {@link RestException} if the request should be disallowed. 259 * 260 * @param req The HTTP request. 261 */ 262 protected void checkAccess(RestRequest req) { 263 String method = req.getMethod(); 264 if (method.equals("VIEW") && ! allowViews) 265 throw new RestException(SC_METHOD_NOT_ALLOWED, "VIEW not enabled"); 266 if (method.equals("PUT") && ! allowPuts) 267 throw new RestException(SC_METHOD_NOT_ALLOWED, "PUT not enabled"); 268 if (method.equals("DELETE") && ! allowDeletes) 269 throw new RestException(SC_METHOD_NOT_ALLOWED, "DELETE not enabled"); 270 if (method.equals("DOWNLOAD") && ! allowViews) 271 throw new RestException(SC_METHOD_NOT_ALLOWED, "DOWNLOAD not enabled"); 272 } 273 274 /** File POJO */ 275 public class FileResource { 276 private File f; 277 private URL url; 278 279 /** 280 * Constructor. 281 * 282 * @param f The file. 283 * @param url The URL of the file resource. 284 */ 285 public FileResource(File f, URL url) { 286 this.f = f; 287 this.url = url; 288 } 289 290 // Bean property getters 291 292 /** 293 * @return The URL of the file resource. 294 */ 295 public URL getUrl() { 296 return url; 297 } 298 299 /** 300 * @return The file type. 301 */ 302 public String getType() { 303 return (f.isDirectory() ? "dir" : "file"); 304 } 305 306 /** 307 * @return The file name. 308 */ 309 public String getName() { 310 return f.getName(); 311 } 312 313 /** 314 * @return The file size. 315 */ 316 public long getSize() { 317 return f.length(); 318 } 319 320 /** 321 * @return The file last modified timestamp. 322 */ 323 @Swap(DateSwap.ISO8601DTP.class) 324 public Date getLastModified() { 325 return new Date(f.lastModified()); 326 } 327 328 /** 329 * @return A hyperlink to view the contents of the file. 330 * @throws Exception If access is not allowed. 331 */ 332 public URL getView() throws Exception { 333 if (allowViews && f.canRead() && ! f.isDirectory()) 334 return new URL(url + "?method=VIEW"); 335 return null; 336 } 337 338 /** 339 * @return A hyperlink to download the contents of the file. 340 * @throws Exception If access is not allowed. 341 */ 342 public URL getDownload() throws Exception { 343 if (allowViews && f.canRead() && ! f.isDirectory()) 344 return new URL(url + "?method=DOWNLOAD"); 345 return null; 346 } 347 348 /** 349 * @return A hyperlink to delete the file. 350 * @throws Exception If access is not allowed. 351 */ 352 public URL getDelete() throws Exception { 353 if (allowDeletes && f.canWrite()) 354 return new URL(url + "?method=DELETE"); 355 return null; 356 } 357 } 358 359 /** Utility method */ 360 private void deleteFile(File f) { 361 try { 362 if (f.isDirectory()) { 363 File[] files = f.listFiles(); 364 if (files != null) { 365 for (File fc : files) 366 deleteFile(fc); 367 } 368 } 369 f.delete(); 370 } catch (Exception e) { 371 logger.log(WARNING, "Cannot delete file '" + f.getAbsolutePath() + "'", e); 372 } 373 } 374}