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