// *************************************************************************************************************************** | |
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * | |
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * | |
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * | |
// * with the License. You may obtain a copy of the License at * | |
// * * | |
// * http://www.apache.org/licenses/LICENSE-2.0 * | |
// * * | |
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * | |
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * | |
// * specific language governing permissions and limitations under the License. * | |
// *************************************************************************************************************************** | |
package org.apache.juneau.microservice.resources; | |
import static java.util.logging.Level.*; | |
import static javax.servlet.http.HttpServletResponse.*; | |
import static org.apache.juneau.html.HtmlDocSerializer.*; | |
import static org.apache.juneau.http.HttpMethodName.*; | |
import java.io.*; | |
import java.net.*; | |
import java.util.*; | |
import java.util.logging.*; | |
import javax.servlet.*; | |
import org.apache.juneau.annotation.*; | |
import org.apache.juneau.rest.*; | |
import org.apache.juneau.rest.annotation.*; | |
import org.apache.juneau.rest.converters.*; | |
import org.apache.juneau.transforms.*; | |
import org.apache.juneau.utils.*; | |
/** | |
* REST resource that allows access to a file system directory. | |
* | |
* <p> | |
* The root directory is specified in one of two ways: | |
* <ul class='spaced-list'> | |
* <li> | |
* Specifying the location via a <l>DirectoryResource.rootDir</l> property. | |
* <li> | |
* Overriding the {@link #getRootDir()} method. | |
* </ul> | |
* | |
* <p> | |
* Read/write access control is handled through the following properties: | |
* <ul class='spaced-list'> | |
* <li> | |
* <l>DirectoryResource.allowViews</l> - If <jk>true</jk>, allows view and download access to files. | |
* <li> | |
* <l>DirectoryResource.allowPuts</l> - If <jk>true</jk>, allows files to be created or overwritten. | |
* <li> | |
* <l>DirectoryResource.allowDeletes</l> - If <jk>true</jk>, allows files to be deleted. | |
* </ul> | |
* | |
* <p> | |
* Access can also be controlled by overriding the {@link #checkAccess(RestRequest)} method. | |
*/ | |
@RestResource( | |
title="File System Explorer", | |
description="Contents of $RA{path}", | |
messages="nls/DirectoryResource", | |
htmldoc=@HtmlDoc( | |
navlinks={ | |
"up: request:/..", | |
"options: servlet:/?method=OPTIONS" | |
} | |
), | |
allowedMethodParams="*", | |
properties={ | |
@Property(name=HTML_uriAnchorText, value="PROPERTY_NAME"), | |
@Property(name="DirectoryResource.rootDir", value="") | |
} | |
) | |
public class DirectoryResource extends BasicRestServlet { | |
private static final long serialVersionUID = 1L; | |
private File rootDir; // The root directory | |
// Settings enabled through servlet init parameters | |
boolean allowDeletes, allowPuts, allowViews; | |
private static Logger logger = Logger.getLogger(DirectoryResource.class.getName()); | |
@Override /* Servlet */ | |
public void init() throws ServletException { | |
RestContextProperties p = getProperties(); | |
rootDir = new File(p.getString("DirectoryResource.rootDir")); | |
allowViews = p.getBoolean("DirectoryResource.allowViews", false); | |
allowDeletes = p.getBoolean("DirectoryResource.allowDeletes", false); | |
allowPuts = p.getBoolean("DirectoryResource.allowPuts", false); | |
} | |
/** | |
* Returns the root directory defined by the 'rootDir' init parameter. | |
* | |
* <p> | |
* Subclasses can override this method to provide their own root directory. | |
* | |
* @return The root directory. | |
*/ | |
protected File getRootDir() { | |
if (rootDir == null) { | |
rootDir = new File(getProperties().getString("rootDir")); | |
if (! rootDir.exists()) | |
if (! rootDir.mkdirs()) | |
throw new RuntimeException("Could not create root dir"); | |
} | |
return rootDir; | |
} | |
/** | |
* [GET /*] - On directories, returns a directory listing. On files, returns information about the file. | |
* | |
* @param req The HTTP request. | |
* @return Either a FileResource or list of FileResources depending on whether it's a | |
* file or directory. | |
* @throws Exception If file could not be read or access was not granted. | |
*/ | |
@RestMethod(name=GET, path="/*", | |
description="On directories, returns a directory listing.\nOn files, returns information about the file.", | |
converters={Queryable.class} | |
) | |
public Object doGet(RestRequest req) throws Exception { | |
checkAccess(req); | |
String pathInfo = req.getPathInfo(); | |
File f = pathInfo == null ? rootDir : new File(rootDir.getAbsolutePath() + pathInfo); | |
if (!f.exists()) | |
throw new RestException(SC_NOT_FOUND, "File not found"); | |
req.setAttribute("path", f.getAbsolutePath()); | |
if (f.isDirectory()) { | |
List<FileResource> l = new LinkedList<>(); | |
File[] files = f.listFiles(); | |
if (files != null) { | |
for (File fc : files) { | |
URL fUrl = new URL(req.getRequestURL().append("/").append(fc.getName()).toString()); | |
l.add(new FileResource(fc, fUrl)); | |
} | |
} | |
return l; | |
} | |
return new FileResource(f, new URL(req.getRequestURL().toString())); | |
} | |
/** | |
* [DELETE /*] - Delete a file on the file system. | |
* | |
* @param req The HTTP request. | |
* @return The message <js>"File deleted"</js> if successful. | |
* @throws Exception If file could not be read or access was not granted. | |
*/ | |
@RestMethod(name=DELETE, path="/*", | |
description="Delete a file on the file system." | |
) | |
public Object doDelete(RestRequest req) throws Exception { | |
checkAccess(req); | |
File f = new File(rootDir.getAbsolutePath() + req.getPathInfo()); | |
deleteFile(f); | |
if (req.getHeader("Accept").contains("text/html")) | |
return new Redirect(); | |
return "File deleted"; | |
} | |
/** | |
* [PUT /*] - Add or overwrite a file on the file system. | |
* | |
* @param req The HTTP request. | |
* @return The message <js>"File added"</js> if successful. | |
* @throws Exception If file could not be read or access was not granted. | |
*/ | |
@RestMethod(name=PUT, path="/*", | |
description="Add or overwrite a file on the file system." | |
) | |
public Object doPut(RestRequest req) throws Exception { | |
checkAccess(req); | |
File f = new File(rootDir.getAbsolutePath() + req.getPathInfo()); | |
String parentSubPath = f.getParentFile().getAbsolutePath().substring(rootDir.getAbsolutePath().length()); | |
try (InputStream is = req.getInputStream(); OutputStream os = new BufferedOutputStream(new FileOutputStream(f))) { | |
IOPipe.create(is, os).run(); | |
} | |
if (req.getContentType().contains("html")) | |
return new Redirect(parentSubPath); | |
return "File added"; | |
} | |
/** | |
* [VIEW /*] - View the contents of a file. | |
* | |
* <p> | |
* Applies to files only. | |
* | |
* @param req The HTTP request. | |
* @param res The HTTP response. | |
* @return A Reader containing the contents of the file. | |
* @throws Exception If file could not be read or access was not granted. | |
*/ | |
@RestMethod(name="VIEW", path="/*", | |
description="View the contents of a file.\nApplies to files only." | |
) | |
public Reader doView(RestRequest req, RestResponse res) throws Exception { | |
checkAccess(req); | |
File f = new File(rootDir.getAbsolutePath() + req.getPathInfo()); | |
if (!f.exists()) | |
throw new RestException(SC_NOT_FOUND, "File not found"); | |
if (f.isDirectory()) | |
throw new RestException(SC_METHOD_NOT_ALLOWED, "VIEW not available on directories"); | |
res.setContentType("text/plain"); | |
return new FileReader(f); | |
} | |
/** | |
* [DOWNLOAD /*] - Download the contents of a file. | |
* | |
* <p> | |
* Applies to files only. | |
* | |
* @param req The HTTP request. | |
* @param res The HTTP response. | |
* @return A Reader containing the contents of the file. | |
* @throws Exception If file could not be read or access was not granted. | |
*/ | |
@RestMethod(name="DOWNLOAD", path="/*", | |
description="Download the contents of a file.\nApplies to files only." | |
) | |
public Reader doDownload(RestRequest req, RestResponse res) throws Exception { | |
checkAccess(req); | |
File f = new File(rootDir.getAbsolutePath() + req.getPathInfo()); | |
if (!f.exists()) | |
throw new RestException(SC_NOT_FOUND, "File not found"); | |
if (f.isDirectory()) | |
throw new RestException(SC_METHOD_NOT_ALLOWED, "DOWNLOAD not available on directories"); | |
res.setContentType("application"); | |
return new FileReader(f); | |
} | |
/** | |
* Verify that the specified request is allowed. | |
* | |
* <p> | |
* Subclasses can override this method to provide customized behavior. | |
* Method should throw a {@link RestException} if the request should be disallowed. | |
* | |
* @param req The HTTP request. | |
*/ | |
protected void checkAccess(RestRequest req) { | |
String method = req.getMethod(); | |
if (method.equals("VIEW") && ! allowViews) | |
throw new RestException(SC_METHOD_NOT_ALLOWED, "VIEW not enabled"); | |
if (method.equals("PUT") && ! allowPuts) | |
throw new RestException(SC_METHOD_NOT_ALLOWED, "PUT not enabled"); | |
if (method.equals("DELETE") && ! allowDeletes) | |
throw new RestException(SC_METHOD_NOT_ALLOWED, "DELETE not enabled"); | |
if (method.equals("DOWNLOAD") && ! allowViews) | |
throw new RestException(SC_METHOD_NOT_ALLOWED, "DOWNLOAD not enabled"); | |
} | |
/** File POJO */ | |
public class FileResource { | |
private File f; | |
private URL url; | |
/** | |
* Constructor. | |
* | |
* @param f The file. | |
* @param url The URL of the file resource. | |
*/ | |
public FileResource(File f, URL url) { | |
this.f = f; | |
this.url = url; | |
} | |
// Bean property getters | |
/** | |
* @return The URL of the file resource. | |
*/ | |
public URL getUrl() { | |
return url; | |
} | |
/** | |
* @return The file type. | |
*/ | |
public String getType() { | |
return (f.isDirectory() ? "dir" : "file"); | |
} | |
/** | |
* @return The file name. | |
*/ | |
public String getName() { | |
return f.getName(); | |
} | |
/** | |
* @return The file size. | |
*/ | |
public long getSize() { | |
return f.length(); | |
} | |
/** | |
* @return The file last modified timestamp. | |
*/ | |
@Swap(DateSwap.ISO8601DTP.class) | |
public Date getLastModified() { | |
return new Date(f.lastModified()); | |
} | |
/** | |
* @return A hyperlink to view the contents of the file. | |
* @throws Exception If access is not allowed. | |
*/ | |
public URL getView() throws Exception { | |
if (allowViews && f.canRead() && ! f.isDirectory()) | |
return new URL(url + "?method=VIEW"); | |
return null; | |
} | |
/** | |
* @return A hyperlink to download the contents of the file. | |
* @throws Exception If access is not allowed. | |
*/ | |
public URL getDownload() throws Exception { | |
if (allowViews && f.canRead() && ! f.isDirectory()) | |
return new URL(url + "?method=DOWNLOAD"); | |
return null; | |
} | |
/** | |
* @return A hyperlink to delete the file. | |
* @throws Exception If access is not allowed. | |
*/ | |
public URL getDelete() throws Exception { | |
if (allowDeletes && f.canWrite()) | |
return new URL(url + "?method=DELETE"); | |
return null; | |
} | |
} | |
/** Utility method */ | |
private void deleteFile(File f) { | |
try { | |
if (f.isDirectory()) { | |
File[] files = f.listFiles(); | |
if (files != null) { | |
for (File fc : files) | |
deleteFile(fc); | |
} | |
} | |
f.delete(); | |
} catch (Exception e) { | |
logger.log(WARNING, "Cannot delete file '" + f.getAbsolutePath() + "'", e); | |
} | |
} | |
} |