001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.microservice.resources;
018
019import static org.apache.juneau.common.utils.IOUtils.*;
020import static org.apache.juneau.common.utils.StringUtils.*;
021import static org.apache.juneau.common.utils.Utils.*;
022
023import java.io.*;
024import java.util.*;
025
026import org.apache.juneau.annotation.*;
027import org.apache.juneau.bean.*;
028import org.apache.juneau.config.*;
029import org.apache.juneau.html.annotation.*;
030import org.apache.juneau.http.annotation.*;
031import org.apache.juneau.http.response.*;
032import org.apache.juneau.rest.*;
033import org.apache.juneau.rest.annotation.*;
034import org.apache.juneau.rest.beans.*;
035import org.apache.juneau.rest.servlet.*;
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 * <h5 class='section'>See Also:</h5><ul>
061 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauMicroserviceCoreBasics">juneau-microservice-core Basics</a>
062 * </ul>
063 *
064 * @serial exclude
065 */
066@Rest(
067   title="File System Explorer",
068   messages="nls/DirectoryResource",
069   allowedMethodParams="*"
070)
071@HtmlDocConfig(
072   navlinks={
073      "up: request:/..",
074      "api: servlet:/api"
075   }
076)
077@HtmlConfig(uriAnchorText="PROPERTY_NAME")
078@SuppressWarnings("javadoc")
079public class DirectoryResource extends BasicRestServlet {
080   private static final long serialVersionUID = 1L;
081
082   //-------------------------------------------------------------------------------------------------------------------
083   // Configurable properties
084   //-------------------------------------------------------------------------------------------------------------------
085
086   private static final String PREFIX = "DirectoryResource.";
087
088   /**
089    * Root directory.
090    */
091   public static final String DIRECTORY_RESOURCE_rootDir = PREFIX + "rootDir.s";
092
093   /**
094    * Allow view and downloads on files.
095    */
096   public static final String DIRECTORY_RESOURCE_allowViews = PREFIX + "allowViews.b";
097
098   /**
099    * Allow deletes on files.
100    */
101   public static final String DIRECTORY_RESOURCE_allowDeletes = PREFIX + "allowDeletes.b";
102
103   /**
104    * Allow uploads on files.
105    */
106   public static final String DIRECTORY_RESOURCE_allowUploads = PREFIX + "allowUploads.b";
107
108
109   //-------------------------------------------------------------------------------------------------------------------
110   // Instance
111   //-------------------------------------------------------------------------------------------------------------------
112
113   private final File rootDir;     // The root directory
114
115   // Properties enabled through servlet init parameters
116   final boolean allowDeletes, allowUploads, allowViews;
117
118   public DirectoryResource(Config c) throws Exception {
119      rootDir = new File(c.get(DIRECTORY_RESOURCE_rootDir).orElse("."));
120      allowViews = c.get(DIRECTORY_RESOURCE_allowViews).asBoolean().orElse(false);
121      allowDeletes = c.get(DIRECTORY_RESOURCE_allowDeletes).asBoolean().orElse(false);
122      allowUploads = c.get(DIRECTORY_RESOURCE_allowUploads).asBoolean().orElse(false);
123   }
124
125   @RestGet(
126      path="/*",
127      summary="View information on file or directory",
128      description="Returns information about the specified file or directory."
129   )
130   @HtmlDocConfig(
131      nav={"<h5>Folder:  $RA{fullPath}</h5>"}
132   )
133   public FileResource getFile(RestRequest req, @Path("/*") String path) throws NotFound, Exception {
134
135      File dir = getFile(path);
136      req.setAttribute("fullPath", dir.getAbsolutePath());
137
138      return new FileResource(dir, path, true);
139   }
140
141   @RestOp(
142      method="VIEW",
143      path="/*",
144      summary="View contents of file",
145      description="View the contents of a file.\nContent-Type is set to 'text/plain'."
146   )
147   public FileContents viewFile(RestResponse res, @Path("/*") String path) throws NotFound, MethodNotAllowed {
148      if (! allowViews)
149         throw new MethodNotAllowed("VIEW not enabled");
150
151      res.setContentType("text/plain");
152      try {
153         return new FileContents(getFile(path));
154      } catch (FileNotFoundException e) {
155         throw new NotFound("File not found");
156      }
157   }
158
159   @RestOp(
160      method="DOWNLOAD",
161      path="/*",
162      summary="Download file",
163      description="Download the contents of a file.\nContent-Type is set to 'application/octet-stream'."
164   )
165   public FileContents downloadFile(RestResponse res, @Path("/*") String path) throws NotFound, MethodNotAllowed {
166      if (! allowViews)
167         throw new MethodNotAllowed("DOWNLOAD not enabled");
168
169      res.setContentType("application/octet-stream");
170      try {
171         return new FileContents(getFile(path));
172      } catch (FileNotFoundException e) {
173         throw new NotFound("File not found");
174      }
175   }
176
177   @RestDelete(
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   @RestPut(
188      path="/*",
189      summary="Add or replace file",
190      description="Add or overwrite a file on the file system."
191   )
192   public RedirectToRoot updateFile(
193      @Content @Schema(type="string",format="binary") InputStream is,
194      @Path("/*") String path
195   ) throws InternalServerError {
196
197      if (! allowUploads)
198         throw new MethodNotAllowed("PUT not enabled");
199
200      File f = getFile(path);
201
202      try (OutputStream os = new BufferedOutputStream(new FileOutputStream(f))) {
203         pipe(is, os);
204      } catch (IOException e) {
205         throw new InternalServerError(e);
206      }
207
208      return new RedirectToRoot();
209   }
210
211   //-----------------------------------------------------------------------------------------------------------------
212   // Helper beans
213   //-----------------------------------------------------------------------------------------------------------------
214
215   @Response @Schema(type="string",format="binary",description="Contents of file")
216   static class FileContents extends FileInputStream {
217      public FileContents(File file) throws FileNotFoundException {
218         super(file);
219      }
220   }
221
222   @Response @Schema(description="Redirect to root page on success")
223   static class RedirectToRoot extends SeeOtherRoot {}
224
225   @Response @Schema(description="File action")
226   public static class Action extends LinkString {
227      public Action(String name, String uri, Object...uriArgs) {
228         super(name, uri, uriArgs);
229      }
230   }
231
232   @Response @Schema(description="File or directory details")
233   @Bean(properties="type,name,size,lastModified,actions,files")
234   public class FileResource {
235      private final File f;
236      private final String path;
237      private final String uri;
238      private final boolean includeChildren;
239
240      public FileResource(File f, String path, boolean includeChildren) {
241         this.f = f;
242         this.path = path;
243         this.uri = "servlet:/"+(path == null ? "" : path);
244         this.includeChildren = includeChildren;
245      }
246
247      public String getType() {
248         return (f.isDirectory() ? "dir" : "file");
249      }
250
251      public LinkString getName() {
252         return new LinkString(f.getName(), uri);
253      }
254
255      public long getSize() {
256         return f.isDirectory() ? f.listFiles().length : f.length();
257      }
258
259      public Date getLastModified() {
260         return new Date(f.lastModified());
261      }
262
263      @Html(format=HtmlFormat.HTML_CDC)
264      public List<Action> getActions() throws Exception {
265         List<Action> l = list();
266         if (allowViews && f.canRead() && ! f.isDirectory()) {
267            l.add(new Action("view", uri + "?method=VIEW"));
268            l.add(new Action("download", uri + "?method=DOWNLOAD"));
269         }
270         if (allowDeletes && f.canWrite() && ! f.isDirectory())
271            l.add(new Action("delete", uri + "?method=DELETE"));
272         return l;
273      }
274
275      public Set<FileResource> getFiles() {
276         if (f.isFile() || ! includeChildren)
277            return null;
278         Set<FileResource> s = new TreeSet<>(new FileResourceComparator());
279         for (File fc : f.listFiles())
280            s.add(new FileResource(fc, (path != null ? (path + '/') : "") + urlEncode(fc.getName()), false));
281         return s;
282      }
283   }
284
285   static class FileResourceComparator implements Comparator<FileResource>, Serializable {
286      private static final long serialVersionUID = 1L;
287      @Override /* Comparator */
288      public int compare(FileResource o1, FileResource o2) {
289         int c = o1.getType().compareTo(o2.getType());
290         return c != 0 ? c : o1.getName().compareTo(o2.getName());
291      }
292   }
293
294   //-----------------------------------------------------------------------------------------------------------------
295   // Helper methods
296   //-----------------------------------------------------------------------------------------------------------------
297
298   /**
299    * Returns the root directory.
300    *
301    * @return The root directory.
302    */
303   protected File getRootDir() {
304      return rootDir;
305   }
306
307   private File getFile(String path) throws NotFound {
308      if (path == null)
309         return rootDir;
310      File f = new File(rootDir.getAbsolutePath() + '/' + path);
311      if (f.exists())
312         return f;
313      throw new NotFound("File not found.");
314   }
315
316   private void deleteFile(File f) {
317      if (! allowDeletes)
318         throw new MethodNotAllowed("DELETE not enabled");
319      if (f.isDirectory()) {
320         File[] files = f.listFiles();
321         if (files != null) {
322            for (File fc : files)
323               deleteFile(fc);
324         }
325      }
326      if (! f.delete())
327         throw new Forbidden("Could not delete file {0}", f.getAbsolutePath()) ;
328   }
329}