001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.microservice.resources; 018 019import static org.apache.juneau.common.utils.IOUtils.*; 020import static org.apache.juneau.common.utils.StringUtils.*; 021import static org.apache.juneau.common.utils.Utils.*; 022 023import java.io.*; 024import java.util.*; 025 026import org.apache.juneau.annotation.*; 027import org.apache.juneau.bean.*; 028import org.apache.juneau.config.*; 029import org.apache.juneau.html.annotation.*; 030import org.apache.juneau.http.annotation.*; 031import org.apache.juneau.http.response.*; 032import org.apache.juneau.rest.*; 033import org.apache.juneau.rest.annotation.*; 034import org.apache.juneau.rest.beans.*; 035import org.apache.juneau.rest.servlet.*; 036 037/** 038 * REST resource that allows access to a file system directory. 039 * 040 * <p> 041 * The root directory is specified in one of two ways: 042 * <ul class='spaced-list'> 043 * <li> 044 * Specifying the location via a <l>DirectoryResource.rootDir</l> property. 045 * <li> 046 * Overriding the {@link #getRootDir()} method. 047 * </ul> 048 * 049 * <p> 050 * Read/write access control is handled through the following properties: 051 * <ul class='spaced-list'> 052 * <li> 053 * <l>DirectoryResource.allowViews</l> - If <jk>true</jk>, allows view and download access to files. 054 * <li> 055 * <l>DirectoryResource.allowUploads</l> - If <jk>true</jk>, allows files to be created or overwritten. 056 * <li> 057 * <l>DirectoryResource.allowDeletes</l> - If <jk>true</jk>, allows files to be deleted. 058 * </ul> 059 * 060 * <h5 class='section'>See Also:</h5><ul> 061 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauMicroserviceCoreBasics">juneau-microservice-core Basics</a> 062 * </ul> 063 * 064 * @serial exclude 065 */ 066@Rest( 067 title="File System Explorer", 068 messages="nls/DirectoryResource", 069 allowedMethodParams="*" 070) 071@HtmlDocConfig( 072 navlinks={ 073 "up: request:/..", 074 "api: servlet:/api" 075 } 076) 077@HtmlConfig(uriAnchorText="PROPERTY_NAME") 078@SuppressWarnings("javadoc") 079public class DirectoryResource extends BasicRestServlet { 080 private static final long serialVersionUID = 1L; 081 082 //------------------------------------------------------------------------------------------------------------------- 083 // Configurable properties 084 //------------------------------------------------------------------------------------------------------------------- 085 086 private static final String PREFIX = "DirectoryResource."; 087 088 /** 089 * Root directory. 090 */ 091 public static final String DIRECTORY_RESOURCE_rootDir = PREFIX + "rootDir.s"; 092 093 /** 094 * Allow view and downloads on files. 095 */ 096 public static final String DIRECTORY_RESOURCE_allowViews = PREFIX + "allowViews.b"; 097 098 /** 099 * Allow deletes on files. 100 */ 101 public static final String DIRECTORY_RESOURCE_allowDeletes = PREFIX + "allowDeletes.b"; 102 103 /** 104 * Allow uploads on files. 105 */ 106 public static final String DIRECTORY_RESOURCE_allowUploads = PREFIX + "allowUploads.b"; 107 108 109 //------------------------------------------------------------------------------------------------------------------- 110 // Instance 111 //------------------------------------------------------------------------------------------------------------------- 112 113 private final File rootDir; // The root directory 114 115 // Properties enabled through servlet init parameters 116 final boolean allowDeletes, allowUploads, allowViews; 117 118 public DirectoryResource(Config c) throws Exception { 119 rootDir = new File(c.get(DIRECTORY_RESOURCE_rootDir).orElse(".")); 120 allowViews = c.get(DIRECTORY_RESOURCE_allowViews).asBoolean().orElse(false); 121 allowDeletes = c.get(DIRECTORY_RESOURCE_allowDeletes).asBoolean().orElse(false); 122 allowUploads = c.get(DIRECTORY_RESOURCE_allowUploads).asBoolean().orElse(false); 123 } 124 125 @RestGet( 126 path="/*", 127 summary="View information on file or directory", 128 description="Returns information about the specified file or directory." 129 ) 130 @HtmlDocConfig( 131 nav={"<h5>Folder: $RA{fullPath}</h5>"} 132 ) 133 public FileResource getFile(RestRequest req, @Path("/*") String path) throws NotFound, Exception { 134 135 File dir = getFile(path); 136 req.setAttribute("fullPath", dir.getAbsolutePath()); 137 138 return new FileResource(dir, path, true); 139 } 140 141 @RestOp( 142 method="VIEW", 143 path="/*", 144 summary="View contents of file", 145 description="View the contents of a file.\nContent-Type is set to 'text/plain'." 146 ) 147 public FileContents viewFile(RestResponse res, @Path("/*") String path) throws NotFound, MethodNotAllowed { 148 if (! allowViews) 149 throw new MethodNotAllowed("VIEW not enabled"); 150 151 res.setContentType("text/plain"); 152 try { 153 return new FileContents(getFile(path)); 154 } catch (FileNotFoundException e) { 155 throw new NotFound("File not found"); 156 } 157 } 158 159 @RestOp( 160 method="DOWNLOAD", 161 path="/*", 162 summary="Download file", 163 description="Download the contents of a file.\nContent-Type is set to 'application/octet-stream'." 164 ) 165 public FileContents downloadFile(RestResponse res, @Path("/*") String path) throws NotFound, MethodNotAllowed { 166 if (! allowViews) 167 throw new MethodNotAllowed("DOWNLOAD not enabled"); 168 169 res.setContentType("application/octet-stream"); 170 try { 171 return new FileContents(getFile(path)); 172 } catch (FileNotFoundException e) { 173 throw new NotFound("File not found"); 174 } 175 } 176 177 @RestDelete( 178 path="/*", 179 summary="Delete file", 180 description="Delete a file on the file system." 181 ) 182 public RedirectToRoot deleteFile(@Path("/*") String path) throws MethodNotAllowed { 183 deleteFile(getFile(path)); 184 return new RedirectToRoot(); 185 } 186 187 @RestPut( 188 path="/*", 189 summary="Add or replace file", 190 description="Add or overwrite a file on the file system." 191 ) 192 public RedirectToRoot updateFile( 193 @Content @Schema(type="string",format="binary") InputStream is, 194 @Path("/*") String path 195 ) throws InternalServerError { 196 197 if (! allowUploads) 198 throw new MethodNotAllowed("PUT not enabled"); 199 200 File f = getFile(path); 201 202 try (OutputStream os = new BufferedOutputStream(new FileOutputStream(f))) { 203 pipe(is, os); 204 } catch (IOException e) { 205 throw new InternalServerError(e); 206 } 207 208 return new RedirectToRoot(); 209 } 210 211 //----------------------------------------------------------------------------------------------------------------- 212 // Helper beans 213 //----------------------------------------------------------------------------------------------------------------- 214 215 @Response @Schema(type="string",format="binary",description="Contents of file") 216 static class FileContents extends FileInputStream { 217 public FileContents(File file) throws FileNotFoundException { 218 super(file); 219 } 220 } 221 222 @Response @Schema(description="Redirect to root page on success") 223 static class RedirectToRoot extends SeeOtherRoot {} 224 225 @Response @Schema(description="File action") 226 public static class Action extends LinkString { 227 public Action(String name, String uri, Object...uriArgs) { 228 super(name, uri, uriArgs); 229 } 230 } 231 232 @Response @Schema(description="File or directory details") 233 @Bean(properties="type,name,size,lastModified,actions,files") 234 public class FileResource { 235 private final File f; 236 private final String path; 237 private final String uri; 238 private final boolean includeChildren; 239 240 public FileResource(File f, String path, boolean includeChildren) { 241 this.f = f; 242 this.path = path; 243 this.uri = "servlet:/"+(path == null ? "" : path); 244 this.includeChildren = includeChildren; 245 } 246 247 public String getType() { 248 return (f.isDirectory() ? "dir" : "file"); 249 } 250 251 public LinkString getName() { 252 return new LinkString(f.getName(), uri); 253 } 254 255 public long getSize() { 256 return f.isDirectory() ? f.listFiles().length : f.length(); 257 } 258 259 public Date getLastModified() { 260 return new Date(f.lastModified()); 261 } 262 263 @Html(format=HtmlFormat.HTML_CDC) 264 public List<Action> getActions() throws Exception { 265 List<Action> l = list(); 266 if (allowViews && f.canRead() && ! f.isDirectory()) { 267 l.add(new Action("view", uri + "?method=VIEW")); 268 l.add(new Action("download", uri + "?method=DOWNLOAD")); 269 } 270 if (allowDeletes && f.canWrite() && ! f.isDirectory()) 271 l.add(new Action("delete", uri + "?method=DELETE")); 272 return l; 273 } 274 275 public Set<FileResource> getFiles() { 276 if (f.isFile() || ! includeChildren) 277 return null; 278 Set<FileResource> s = new TreeSet<>(new FileResourceComparator()); 279 for (File fc : f.listFiles()) 280 s.add(new FileResource(fc, (path != null ? (path + '/') : "") + urlEncode(fc.getName()), false)); 281 return s; 282 } 283 } 284 285 static class FileResourceComparator implements Comparator<FileResource>, Serializable { 286 private static final long serialVersionUID = 1L; 287 @Override /* Comparator */ 288 public int compare(FileResource o1, FileResource o2) { 289 int c = o1.getType().compareTo(o2.getType()); 290 return c != 0 ? c : o1.getName().compareTo(o2.getName()); 291 } 292 } 293 294 //----------------------------------------------------------------------------------------------------------------- 295 // Helper methods 296 //----------------------------------------------------------------------------------------------------------------- 297 298 /** 299 * Returns the root directory. 300 * 301 * @return The root directory. 302 */ 303 protected File getRootDir() { 304 return rootDir; 305 } 306 307 private File getFile(String path) throws NotFound { 308 if (path == null) 309 return rootDir; 310 File f = new File(rootDir.getAbsolutePath() + '/' + path); 311 if (f.exists()) 312 return f; 313 throw new NotFound("File not found."); 314 } 315 316 private void deleteFile(File f) { 317 if (! allowDeletes) 318 throw new MethodNotAllowed("DELETE not enabled"); 319 if (f.isDirectory()) { 320 File[] files = f.listFiles(); 321 if (files != null) { 322 for (File fc : files) 323 deleteFile(fc); 324 } 325 } 326 if (! f.delete()) 327 throw new Forbidden("Could not delete file {0}", f.getAbsolutePath()) ; 328 } 329}