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