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