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