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