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