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.html.HtmlDocSerializer.*; 016import static org.apache.juneau.http.HttpMethodName.*; 017import static org.apache.juneau.internal.StringUtils.*; 018import static org.apache.juneau.rest.annotation.HookEvent.*; 019 020import java.io.*; 021import java.util.*; 022 023import org.apache.juneau.annotation.*; 024import org.apache.juneau.dto.*; 025import org.apache.juneau.html.annotation.*; 026import org.apache.juneau.http.annotation.Body; 027import org.apache.juneau.http.annotation.Path; 028import org.apache.juneau.http.annotation.Response; 029import org.apache.juneau.jsonschema.annotation.*; 030import org.apache.juneau.rest.*; 031import org.apache.juneau.rest.annotation.*; 032import org.apache.juneau.rest.exception.*; 033import org.apache.juneau.rest.helper.*; 034import org.apache.juneau.transforms.*; 035import org.apache.juneau.utils.*; 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@RestResource( 061 title="File System Explorer", 062 messages="nls/DirectoryResource", 063 htmldoc=@HtmlDoc( 064 navlinks={ 065 "up: request:/..", 066 "options: servlet:/?method=OPTIONS" 067 } 068 ), 069 allowedMethodParams="*", 070 properties={ 071 @Property(name=HTML_uriAnchorText, value="PROPERTY_NAME") 072 } 073) 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 * Configuration property: Root directory. 086 */ 087 public static final String DIRECTORY_RESOURCE_rootDir = PREFIX + "rootDir.s"; 088 089 /** 090 * Configuration property: Allow view and downloads on files. 091 */ 092 public static final String DIRECTORY_RESOURCE_allowViews = PREFIX + "allowViews.b"; 093 094 /** 095 * Configuration property: Allow deletes on files. 096 */ 097 public static final String DIRECTORY_RESOURCE_allowDeletes = PREFIX + "allowDeletes.b"; 098 099 /** 100 * Configuration property: 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 File rootDir; // The root directory 110 111 // Settings enabled through servlet init parameters 112 boolean allowDeletes, allowUploads, allowViews; 113 114 @RestHook(INIT) 115 public void init(RestContextBuilder b) throws Exception { 116 RestContextProperties p = b.getProperties(); 117 rootDir = new File(p.getString(DIRECTORY_RESOURCE_rootDir)); 118 allowViews = p.getBoolean(DIRECTORY_RESOURCE_allowViews, false); 119 allowDeletes = p.getBoolean(DIRECTORY_RESOURCE_allowDeletes, false); 120 allowUploads = p.getBoolean(DIRECTORY_RESOURCE_allowUploads, false); 121 } 122 123 @RestMethod( 124 name=GET, 125 path="/*", 126 summary="View information on file or directory", 127 description="Returns information about the specified file or directory.", 128 htmldoc=@HtmlDoc( 129 nav={"<h5>Folder: $RA{fullPath}</h5>"} 130 ) 131 ) 132 public FileResource getFile(RestRequest req, @Path("/*") String path) throws NotFound, Exception { 133 134 File dir = getFile(path); 135 req.setAttribute("fullPath", dir.getAbsolutePath()); 136 137 return new FileResource(dir, path, true); 138 } 139 140 @RestMethod( 141 name="VIEW", 142 path="/*", 143 summary="View contents of file", 144 description="View the contents of a file.\nContent-Type is set to 'text/plain'." 145 ) 146 public FileContents viewFile(RestResponse res, @Path("/*") String path) throws NotFound, MethodNotAllowed { 147 if (! allowViews) 148 throw new MethodNotAllowed("VIEW not enabled"); 149 150 res.setContentType("text/plain"); 151 try { 152 return new FileContents(getFile(path)); 153 } catch (FileNotFoundException e) { 154 throw new NotFound("File not found"); 155 } 156 } 157 158 @RestMethod( 159 name="DOWNLOAD", 160 path="/*", 161 summary="Download file", 162 description="Download the contents of a file.\nContent-Type is set to 'application/octet-stream'." 163 ) 164 public FileContents downloadFile(RestResponse res, @Path("/*") String path) throws NotFound, MethodNotAllowed { 165 if (! allowViews) 166 throw new MethodNotAllowed("DOWNLOAD not enabled"); 167 168 res.setContentType("application/octet-stream"); 169 try { 170 return new FileContents(getFile(path)); 171 } catch (FileNotFoundException e) { 172 throw new NotFound("File not found"); 173 } 174 } 175 176 @RestMethod( 177 name=DELETE, 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 @RestMethod( 188 name=PUT, 189 path="/*", 190 summary="Add or replace file", 191 description="Add or overwrite a file on the file system." 192 ) 193 public RedirectToRoot updateFile( 194 @Body(schema=@Schema(type="string",format="binary")) InputStream is, 195 @Path("/*") String path 196 ) throws InternalServerError { 197 198 if (! allowUploads) 199 throw new MethodNotAllowed("PUT not enabled"); 200 201 File f = getFile(path); 202 203 try (OutputStream os = new BufferedOutputStream(new FileOutputStream(f))) { 204 IOPipe.create(is, os).run(); 205 } catch (IOException e) { 206 throw new InternalServerError(e); 207 } 208 209 return new RedirectToRoot(); 210 } 211 212 //----------------------------------------------------------------------------------------------------------------- 213 // Helper beans 214 //----------------------------------------------------------------------------------------------------------------- 215 216 @Response(schema=@Schema(type="string",format="binary"), description="Contents of file") 217 static class FileContents extends FileInputStream { 218 public FileContents(File file) throws FileNotFoundException { 219 super(file); 220 } 221 } 222 223 @Response(description="Redirect to root page on success") 224 static class RedirectToRoot extends SeeOtherRoot {} 225 226 @Response(description="File action") 227 public static class Action extends LinkString { 228 public Action(String name, String uri, Object...uriArgs) { 229 super(name, uri, uriArgs); 230 } 231 } 232 233 @Response(description="File or directory details") 234 @Bean(properties="type,name,size,lastModified,actions,files") 235 public class FileResource { 236 private final File f; 237 private final String path; 238 private final String uri; 239 private final boolean includeChildren; 240 241 public FileResource(File f, String path, boolean includeChildren) { 242 this.f = f; 243 this.path = path; 244 this.uri = "servlet:/"+(path == null ? "" : path); 245 this.includeChildren = includeChildren; 246 } 247 248 public String getType() { 249 return (f.isDirectory() ? "dir" : "file"); 250 } 251 252 public LinkString getName() { 253 return new LinkString(f.getName(), uri); 254 } 255 256 public long getSize() { 257 return f.isDirectory() ? f.listFiles().length : f.length(); 258 } 259 260 @Swap(DateSwap.ISO8601DTP.class) 261 public Date getLastModified() { 262 return new Date(f.lastModified()); 263 } 264 265 @Html(format=HtmlFormat.HTML_CDC) 266 public List<Action> getActions() throws Exception { 267 List<Action> l = new ArrayList<>(); 268 if (allowViews && f.canRead() && ! f.isDirectory()) { 269 l.add(new Action("view", uri + "?method=VIEW")); 270 l.add(new Action("download", uri + "?method=DOWNLOAD")); 271 } 272 if (allowDeletes && f.canWrite() && ! f.isDirectory()) 273 l.add(new Action("delete", uri + "?method=DELETE")); 274 return l; 275 } 276 277 public Set<FileResource> getFiles() { 278 if (f.isFile() || ! includeChildren) 279 return null; 280 Set<FileResource> s = new TreeSet<>(new FileResourceComparator()); 281 for (File fc : f.listFiles()) 282 s.add(new FileResource(fc, (path != null ? (path + '/') : "") + urlEncode(fc.getName()), false)); 283 return s; 284 } 285 } 286 287 static final class FileResourceComparator implements Comparator<FileResource>, Serializable { 288 private static final long serialVersionUID = 1L; 289 @Override /* Comparator */ 290 public int compare(FileResource o1, FileResource o2) { 291 int c = o1.getType().compareTo(o2.getType()); 292 return c != 0 ? c : o1.getName().compareTo(o2.getName()); 293 } 294 } 295 296 //----------------------------------------------------------------------------------------------------------------- 297 // Helper methods 298 //----------------------------------------------------------------------------------------------------------------- 299 300 /** 301 * Returns the root directory. 302 * 303 * @return The root directory. 304 */ 305 protected File getRootDir() { 306 return rootDir; 307 } 308 309 private File getFile(String path) throws NotFound { 310 if (path == null) 311 return rootDir; 312 File f = new File(rootDir.getAbsolutePath() + '/' + path); 313 if (f.exists()) 314 return f; 315 throw new NotFound("File not found."); 316 } 317 318 private void deleteFile(File f) { 319 if (! allowDeletes) 320 throw new MethodNotAllowed("DELETE not enabled"); 321 if (f.isDirectory()) { 322 File[] files = f.listFiles(); 323 if (files != null) { 324 for (File fc : files) 325 deleteFile(fc); 326 } 327 } 328 if (! f.delete()) 329 throw new Forbidden("Could not delete file {0}", f.getAbsolutePath()) ; 330 } 331}