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