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