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 java.util.logging.Level.*;
016import static javax.servlet.http.HttpServletResponse.*;
017import static org.apache.juneau.html.HtmlDocSerializer.*;
018import static org.apache.juneau.http.HttpMethodName.*;
019
020import java.io.*;
021import java.net.*;
022import java.util.*;
023import java.util.logging.*;
024
025import javax.servlet.*;
026
027import org.apache.juneau.annotation.*;
028import org.apache.juneau.rest.*;
029import org.apache.juneau.rest.annotation.*;
030import org.apache.juneau.rest.converters.*;
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.allowPuts</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 * <p>
058 * Access can also be controlled by overriding the {@link #checkAccess(RestRequest)} method.
059 */
060@RestResource(
061   title="File System Explorer",
062   description="Contents of $RA{path}",
063   messages="nls/DirectoryResource",
064   htmldoc=@HtmlDoc(
065      navlinks={
066         "up: request:/..",
067         "options: servlet:/?method=OPTIONS"
068      }
069   ),
070   allowedMethodParams="*",
071   properties={
072      @Property(name=HTML_uriAnchorText, value="PROPERTY_NAME"),
073      @Property(name="DirectoryResource.rootDir", value="")
074   }
075)
076public class DirectoryResource extends BasicRestServlet {
077   private static final long serialVersionUID = 1L;
078
079   private File rootDir;     // The root directory
080
081   // Settings enabled through servlet init parameters
082   boolean allowDeletes, allowPuts, allowViews;
083
084   private static Logger logger = Logger.getLogger(DirectoryResource.class.getName());
085
086   @Override /* Servlet */
087   public void init() throws ServletException {
088      RestContextProperties p = getProperties();
089      rootDir = new File(p.getString("DirectoryResource.rootDir"));
090      allowViews = p.getBoolean("DirectoryResource.allowViews", false);
091      allowDeletes = p.getBoolean("DirectoryResource.allowDeletes", false);
092      allowPuts = p.getBoolean("DirectoryResource.allowPuts", false);
093   }
094
095   /**
096    * Returns the root directory defined by the 'rootDir' init parameter.
097    * 
098    * <p>
099    * Subclasses can override this method to provide their own root directory.
100    * 
101    * @return The root directory.
102    */
103   protected File getRootDir() {
104      if (rootDir == null) {
105         rootDir = new File(getProperties().getString("rootDir"));
106         if (! rootDir.exists())
107            if (! rootDir.mkdirs())
108               throw new RuntimeException("Could not create root dir");
109      }
110      return rootDir;
111   }
112
113   /**
114    * [GET /*] - On directories, returns a directory listing.  On files, returns information about the file.
115    * 
116    * @param req The HTTP request.
117    * @return Either a FileResource or list of FileResources depending on whether it's a
118    *    file or directory.
119    * @throws Exception If file could not be read or access was not granted.
120    */
121   @RestMethod(name=GET, path="/*",
122      description="On directories, returns a directory listing.\nOn files, returns information about the file.",
123      converters={Queryable.class}
124   )
125   public Object doGet(RestRequest req) throws Exception {
126      checkAccess(req);
127
128      String pathInfo = req.getPathInfo();
129      File f = pathInfo == null ? rootDir : new File(rootDir.getAbsolutePath() + pathInfo);
130
131      if (!f.exists())
132         throw new RestException(SC_NOT_FOUND, "File not found");
133
134      req.setAttribute("path", f.getAbsolutePath());
135
136      if (f.isDirectory()) {
137         List<FileResource> l = new LinkedList<>();
138         File[] files = f.listFiles();
139         if (files != null) {
140            for (File fc : files) {
141               URL fUrl = new URL(req.getRequestURL().append("/").append(fc.getName()).toString());
142               l.add(new FileResource(fc, fUrl));
143            }
144         }
145         return l;
146      }
147
148      return new FileResource(f, new URL(req.getRequestURL().toString()));
149   }
150
151   /**
152    * [DELETE /*] - Delete a file on the file system.
153    * 
154    * @param req The HTTP request.
155    * @return The message <js>"File deleted"</js> if successful.
156    * @throws Exception If file could not be read or access was not granted.
157    */
158   @RestMethod(name=DELETE, path="/*",
159      description="Delete a file on the file system."
160   )
161   public Object doDelete(RestRequest req) throws Exception {
162      checkAccess(req);
163
164      File f = new File(rootDir.getAbsolutePath() + req.getPathInfo());
165      deleteFile(f);
166
167      if (req.getHeader("Accept").contains("text/html"))
168         return new Redirect();
169      return "File deleted";
170   }
171
172   /**
173    * [PUT /*] - Add or overwrite a file on the file system.
174    * 
175    * @param req The HTTP request.
176    * @return The message <js>"File added"</js> if successful.
177    * @throws Exception If file could not be read or access was not granted.
178    */
179   @RestMethod(name=PUT, path="/*",
180      description="Add or overwrite a file on the file system."
181   )
182   public Object doPut(RestRequest req) throws Exception {
183      checkAccess(req);
184
185      File f = new File(rootDir.getAbsolutePath() + req.getPathInfo());
186      String parentSubPath = f.getParentFile().getAbsolutePath().substring(rootDir.getAbsolutePath().length());
187      try (InputStream is = req.getInputStream(); OutputStream os = new BufferedOutputStream(new FileOutputStream(f))) {
188         IOPipe.create(is, os).run();
189      }
190      if (req.getContentType().contains("html"))
191         return new Redirect(parentSubPath);
192      return "File added";
193   }
194
195   /**
196    * [VIEW /*] - View the contents of a file.  
197    * 
198    * <p>
199    * Applies to files only.
200    * 
201    * @param req The HTTP request.
202    * @param res The HTTP response.
203    * @return A Reader containing the contents of the file.
204    * @throws Exception If file could not be read or access was not granted.
205    */
206   @RestMethod(name="VIEW", path="/*",
207      description="View the contents of a file.\nApplies to files only."
208   )
209   public Reader doView(RestRequest req, RestResponse res) throws Exception {
210      checkAccess(req);
211
212      File f = new File(rootDir.getAbsolutePath() + req.getPathInfo());
213
214      if (!f.exists())
215         throw new RestException(SC_NOT_FOUND, "File not found");
216
217      if (f.isDirectory())
218         throw new RestException(SC_METHOD_NOT_ALLOWED, "VIEW not available on directories");
219
220      res.setContentType("text/plain");
221      return new FileReader(f);
222   }
223
224   /**
225    * [DOWNLOAD /*] - Download the contents of a file.
226    * 
227    * <p>
228    * Applies to files only.
229    * 
230    * @param req The HTTP request.
231    * @param res The HTTP response.
232    * @return A Reader containing the contents of the file.
233    * @throws Exception If file could not be read or access was not granted.
234    */
235   @RestMethod(name="DOWNLOAD", path="/*",
236      description="Download the contents of a file.\nApplies to files only."
237   )
238   public Reader doDownload(RestRequest req, RestResponse res) throws Exception {
239      checkAccess(req);
240
241      File f = new File(rootDir.getAbsolutePath() + req.getPathInfo());
242
243      if (!f.exists())
244         throw new RestException(SC_NOT_FOUND, "File not found");
245
246      if (f.isDirectory())
247         throw new RestException(SC_METHOD_NOT_ALLOWED, "DOWNLOAD not available on directories");
248
249      res.setContentType("application");
250      return new FileReader(f);
251   }
252
253   /**
254    * Verify that the specified request is allowed.
255    * 
256    * <p>
257    * Subclasses can override this method to provide customized behavior.
258    * Method should throw a {@link RestException} if the request should be disallowed.
259    * 
260    * @param req The HTTP request.
261    */
262   protected void checkAccess(RestRequest req) {
263      String method = req.getMethod();
264      if (method.equals("VIEW") && ! allowViews)
265         throw new RestException(SC_METHOD_NOT_ALLOWED, "VIEW not enabled");
266      if (method.equals("PUT") && ! allowPuts)
267         throw new RestException(SC_METHOD_NOT_ALLOWED, "PUT not enabled");
268      if (method.equals("DELETE") && ! allowDeletes)
269         throw new RestException(SC_METHOD_NOT_ALLOWED, "DELETE not enabled");
270      if (method.equals("DOWNLOAD") && ! allowViews)
271         throw new RestException(SC_METHOD_NOT_ALLOWED, "DOWNLOAD not enabled");
272   }
273
274   /** File POJO */
275   public class FileResource {
276      private File f;
277      private URL url;
278
279      /**
280       * Constructor.
281       * 
282       * @param f The file.
283       * @param url The URL of the file resource.
284       */
285      public FileResource(File f, URL url) {
286         this.f = f;
287         this.url = url;
288      }
289
290      // Bean property getters
291
292      /**
293       * @return The URL of the file resource.
294       */
295      public URL getUrl() {
296         return url;
297      }
298
299      /**
300       * @return The file type.
301       */
302      public String getType() {
303         return (f.isDirectory() ? "dir" : "file");
304      }
305
306      /**
307       * @return The file name.
308       */
309      public String getName() {
310         return f.getName();
311      }
312
313      /**
314       * @return The file size.
315       */
316      public long getSize() {
317         return f.length();
318      }
319
320      /**
321       * @return The file last modified timestamp.
322       */
323      @Swap(DateSwap.ISO8601DTP.class)
324      public Date getLastModified() {
325         return new Date(f.lastModified());
326      }
327
328      /**
329       * @return A hyperlink to view the contents of the file.
330       * @throws Exception If access is not allowed.
331       */
332      public URL getView() throws Exception {
333         if (allowViews && f.canRead() && ! f.isDirectory())
334            return new URL(url + "?method=VIEW");
335         return null;
336      }
337
338      /**
339       * @return A hyperlink to download the contents of the file.
340       * @throws Exception If access is not allowed.
341       */
342      public URL getDownload() throws Exception {
343         if (allowViews && f.canRead() && ! f.isDirectory())
344            return new URL(url + "?method=DOWNLOAD");
345         return null;
346      }
347
348      /**
349       * @return A hyperlink to delete the file.
350       * @throws Exception If access is not allowed.
351       */
352      public URL getDelete() throws Exception {
353         if (allowDeletes && f.canWrite())
354            return new URL(url + "?method=DELETE");
355         return null;
356      }
357   }
358
359   /** Utility method */
360   private void deleteFile(File f) {
361      try {
362         if (f.isDirectory()) {
363            File[] files = f.listFiles();
364            if (files != null) {
365               for (File fc : files)
366                  deleteFile(fc);
367            }
368         }
369         f.delete();
370      } catch (Exception e) {
371         logger.log(WARNING, "Cannot delete file '" + f.getAbsolutePath() + "'", e);
372      }
373   }
374}