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