PUT and MKCOL support. Simplified paths (utilizing a servlet filter to make sure, directory paths always end on "/" while file paths don't).

This commit is contained in:
Sebastian Stenzel
2015-12-27 21:53:50 +01:00
parent d3000da2e9
commit 389c49d846
10 changed files with 275 additions and 53 deletions

View File

@@ -0,0 +1,38 @@
package org.cryptomator.webdav.filters;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Adds an <code>Accept-Range: bytes</code> header to all <code>GET</code> requests.
*/
public class AcceptRangeFilter implements HttpFilter {
private static final String METHOD_GET = "GET";
private static final String HEADER_ACCEPT_RANGES = "Accept-Ranges";
private static final String HEADER_ACCEPT_RANGE_VALUE = "bytes";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// no-op
}
@Override
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (METHOD_GET.equalsIgnoreCase(request.getMethod())) {
response.addHeader(HEADER_ACCEPT_RANGES, HEADER_ACCEPT_RANGE_VALUE);
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
// no-op
}
}

View File

@@ -0,0 +1,26 @@
package org.cryptomator.webdav.filters;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
interface HttpFilter extends Filter {
@Override
default void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
doFilterHttp((HttpServletRequest) request, (HttpServletResponse) response, chain);
} else {
chain.doFilter(request, response);
}
}
void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException;
}

View File

@@ -0,0 +1,77 @@
package org.cryptomator.webdav.filters;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
/**
* Depending on the HTTP method a "/" is added to or removed from the end of an URI.
* For example <code>MKCOL</code> creates a directory (ending on "/"), while <code>PUT</code> creates a file (not ending on "/").
*/
public class UriNormalizationFilter implements HttpFilter {
private static final String[] FILE_METHODS = {"PUT"};
private static final String[] DIRECTORY_METHODS = {"MKCOL"};
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// no-op
}
@Override
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (ArrayUtils.contains(FILE_METHODS, request.getMethod().toUpperCase())) {
chain.doFilter(new FileUriRequest(request), response);
} else if (ArrayUtils.contains(DIRECTORY_METHODS, request.getMethod().toUpperCase())) {
chain.doFilter(new DirectoryUriRequest(request), response);
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
// no-op
}
/**
* HTTP request, whose URI never ends on "/".
*/
private static class FileUriRequest extends HttpServletRequestWrapper {
public FileUriRequest(HttpServletRequest request) {
super(request);
}
@Override
public String getRequestURI() {
return StringUtils.removeEnd(super.getRequestURI(), "/");
}
}
/**
* HTTP request, whose URI always ends on "/".
*/
private static class DirectoryUriRequest extends HttpServletRequestWrapper {
public DirectoryUriRequest(HttpServletRequest request) {
super(request);
}
@Override
public String getRequestURI() {
return StringUtils.appendIfMissing(super.getRequestURI(), "/");
}
}
}

View File

@@ -17,7 +17,6 @@ import java.time.Instant;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
@@ -25,11 +24,12 @@ import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.webdav.jackrabbit.DavPathFactory.DavPath;
class DavFile extends DavNode<File> {
public DavFile(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavResourceLocator locator, File node) {
super(factory, lockManager, session, locator, node);
public DavFile(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavPath path, File node) {
super(factory, lockManager, session, path, node);
}
@Override

View File

@@ -9,6 +9,11 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.time.Instant;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -17,7 +22,6 @@ import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
@@ -27,11 +31,14 @@ import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.ResourceType;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.webdav.jackrabbit.DavPathFactory.DavPath;
class DavFolder extends DavNode<Folder> {
public DavFolder(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavResourceLocator locator, Folder folder) {
super(factory, lockManager, session, locator, folder);
public DavFolder(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavPath path, Folder folder) {
super(factory, lockManager, session, path, folder);
properties.add(new ResourceType(ResourceType.COLLECTION));
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
}
@@ -48,8 +55,30 @@ class DavFolder extends DavNode<Folder> {
@Override
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
// TODO Auto-generated method stub
if (resource instanceof DavFolder) {
addMemberFolder((DavFolder) resource);
} else if (resource instanceof DavFile) {
addMemberFile((DavFile) resource, inputContext.getInputStream());
} else {
throw new IllegalArgumentException("Unsupported resource type: " + resource.getClass().getName());
}
}
private void addMemberFolder(DavFolder memberFolder) {
node.folder(memberFolder.getDisplayName()).create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
}
private void addMemberFile(DavFile memberFile, InputStream inputStream) {
try (ReadableByteChannel src = Channels.newChannel(inputStream); WritableFile dst = node.file(memberFile.getDisplayName()).openWritable()) {
ByteBuffer buf = ByteBuffer.allocate(1337);
while (src.read(buf) != -1) {
buf.flip();
dst.write(buf);
buf.clear();
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
@@ -60,14 +89,12 @@ class DavFolder extends DavNode<Folder> {
}
private DavFolder getMemberFolder(Folder memberFolder) {
final String subFolderResourcePath = locator.getResourcePath() + memberFolder.name() + '/';
final DavResourceLocator subFolderLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), subFolderResourcePath);
final DavPath subFolderLocator = path.getChild(memberFolder.name() + '/');
return factory.createFolder(memberFolder, subFolderLocator, session);
}
private DavFile getMemberFile(File memberFile) {
final String subFolderResourcePath = locator.getResourcePath() + memberFile.name();
final DavResourceLocator subFolderLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), subFolderResourcePath);
final DavPath subFolderLocator = path.getChild(memberFile.name());
return factory.createFile(memberFile, subFolderLocator, session);
}

View File

@@ -14,7 +14,6 @@ import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.FilenameUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
@@ -31,6 +30,7 @@ import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
import org.apache.jackrabbit.webdav.property.DavPropertySet;
import org.apache.jackrabbit.webdav.property.PropEntry;
import org.cryptomator.filesystem.Node;
import org.cryptomator.webdav.jackrabbit.DavPathFactory.DavPath;
abstract class DavNode<T extends Node> implements DavResource {
@@ -41,15 +41,15 @@ abstract class DavNode<T extends Node> implements DavResource {
protected final FilesystemResourceFactory factory;
protected final LockManager lockManager;
protected final DavSession session;
protected final DavResourceLocator locator;
protected final DavPath path;
protected final T node;
protected final DavPropertySet properties;
public DavNode(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavResourceLocator locator, T node) {
public DavNode(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavPath path, T node) {
this.factory = factory;
this.lockManager = lockManager;
this.session = session;
this.locator = locator;
this.path = path;
this.node = node;
this.properties = new DavPropertySet();
}
@@ -76,17 +76,17 @@ abstract class DavNode<T extends Node> implements DavResource {
@Override
public DavResourceLocator getLocator() {
return locator;
return path;
}
@Override
public String getResourcePath() {
return locator.getResourcePath();
return path.getResourcePath();
}
@Override
public String getHref() {
return locator.getHref(this.isCollection());
return path.getHref();
}
@Override
@@ -157,16 +157,15 @@ abstract class DavNode<T extends Node> implements DavResource {
@Override
public DavResource getCollection() {
if (locator.isRootLocation()) {
if (path.isRootLocation()) {
return null;
}
final String parentResource = FilenameUtils.getPathNoEndSeparator(locator.getResourcePath());
final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
final DavPath parentPath = path.getParent();
try {
return factory.createResource(parentLocator, session);
return factory.createResource(parentPath, session);
} catch (DavException e) {
throw new IllegalStateException("Unable to get parent resource with path " + parentLocator.getResourcePath(), e);
throw new IllegalStateException("Unable to get parent resource with path " + parentPath, e);
}
}

View File

@@ -19,45 +19,78 @@ import org.apache.jackrabbit.webdav.util.EncodeUtil;
/**
* A LocatorFactory constructing Locators, whose {@link DavResourceLocator#getResourcePath() resourcePath} and {@link DavResourceLocator#getRepositoryPath() repositoryPath} are equal.
* These paths will be plain, case-sensitive, absolute, unencoded Strings with Unix-style path separators.
*
* Paths ending on "/" are treated as directory paths and all others as file paths.
*/
class IdentityLocatorFactory implements DavLocatorFactory {
class DavPathFactory implements DavLocatorFactory {
private final String pathPrefix;
public IdentityLocatorFactory(URI contextRootUri) {
public DavPathFactory(URI contextRootUri) {
this.pathPrefix = StringUtils.removeEnd(contextRootUri.toString(), "/");
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String href) {
public DavPath createResourceLocator(String prefix, String href) {
final String fullPrefix = StringUtils.removeEnd(prefix, "/");
final String remainingHref = StringUtils.removeStart(href, fullPrefix);
final String unencodedRemaingingHref = EncodeUtil.unescape(remainingHref);
assert unencodedRemaingingHref.startsWith("/");
return new IdentityLocator(unencodedRemaingingHref);
return new DavPath(unencodedRemaingingHref);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
assert resourcePath.startsWith("/");
return new IdentityLocator(resourcePath);
public DavPath createResourceLocator(String prefix, String workspacePath, String resourcePath) {
return createResourceLocator(prefix, workspacePath, resourcePath, true);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
assert path.startsWith("/");
return new IdentityLocator(path);
public DavPath createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
return new DavPath(path);
}
private class IdentityLocator implements DavResourceLocator {
public class DavPath implements DavResourceLocator {
private final String absPath;
private IdentityLocator(String absPath) {
private DavPath(String absPath) {
assert absPath.startsWith("/");
this.absPath = FilenameUtils.normalize(absPath, true);
}
/**
* @return <code>true</code> if the path ends on "/".
*/
public boolean isDirectory() {
return absPath.endsWith("/");
}
/**
* @return Parent DavPath or <code>null</code> if this is the root node.
*/
public DavPath getParent() {
if (isRootLocation()) {
return null;
} else {
final String parentPath = FilenameUtils.getFullPath(FilenameUtils.normalizeNoEndSeparator(absPath, true));
return createResourceLocator(getPrefix(), getWorkspacePath(), parentPath);
}
}
/**
* Get the path of a child resource, consisting of curren path + child path.
* If the child path ends on "/", the returned DavPath will be a directory path.
*
* @return Child path
*/
public DavPath getChild(String relativeChildPath) {
if (isDirectory()) {
final String absChildPath = absPath + StringUtils.removeStart(relativeChildPath, "/");
return createResourceLocator(getPrefix(), getWorkspacePath(), absChildPath);
} else {
throw new UnsupportedOperationException("Can only resolve subpaths of a path representing a directory");
}
}
@Override
public String getPrefix() {
return pathPrefix;
@@ -88,13 +121,20 @@ class IdentityLocatorFactory implements DavLocatorFactory {
return false;
}
/**
* @see #getHref(boolean)
*/
public String getHref() {
return getHref(isDirectory());
}
@Override
public String getHref(boolean isCollection) {
final String encodedResourcePath = EncodeUtil.escapePath(absPath);
if (isRootLocation()) {
return pathPrefix + "/";
} else {
assert isCollection ? encodedResourcePath.endsWith("/") : true;
assert isCollection ? isDirectory() : true;
return pathPrefix + encodedResourcePath;
}
}
@@ -105,8 +145,8 @@ class IdentityLocatorFactory implements DavLocatorFactory {
}
@Override
public DavLocatorFactory getFactory() {
return IdentityLocatorFactory.this;
public DavPathFactory getFactory() {
return DavPathFactory.this;
}
@Override
@@ -116,7 +156,7 @@ class IdentityLocatorFactory implements DavLocatorFactory {
@Override
public String toString() {
return "Locator: " + absPath + " (Prefix: " + pathPrefix + ")";
return "[" + pathPrefix + "]" + absPath;
}
@Override
@@ -126,8 +166,8 @@ class IdentityLocatorFactory implements DavLocatorFactory {
@Override
public boolean equals(Object obj) {
if (obj instanceof IdentityLocator) {
final IdentityLocator other = (IdentityLocator) obj;
if (obj instanceof DavPath) {
final DavPath other = (DavPath) obj;
final boolean samePrefix = this.getPrefix() == null && other.getPrefix() == null || this.getPrefix().equals(other.getPrefix());
final boolean sameRelativeCleartextPath = this.absPath == null && other.absPath == null || this.absPath.equals(other.absPath);
return samePrefix && sameRelativeCleartextPath;

View File

@@ -26,6 +26,7 @@ import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.Node;
import org.cryptomator.webdav.jackrabbit.DavPathFactory.DavPath;
class FilesystemResourceFactory implements DavResourceFactory {
@@ -47,22 +48,29 @@ class FilesystemResourceFactory implements DavResourceFactory {
@Override
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
final String path = locator.getResourcePath();
if (path.endsWith("/")) {
Folder folder = this.resolve(path, FOLDER);
return createFolder(folder, locator, session);
if (locator instanceof DavPath) {
return createResource((DavPath) locator, session);
} else {
File file = this.resolve(path, FILE);
return createFile(file, locator, session);
throw new IllegalArgumentException("Unsupported locator type " + locator.getClass().getName());
}
}
DavFolder createFolder(Folder folder, DavResourceLocator locator, DavSession session) {
return new DavFolder(this, lockManager, session, locator, folder);
private DavResource createResource(DavPath path, DavSession session) throws DavException {
if (path.isDirectory()) {
Folder folder = this.resolve(path.getResourcePath(), FOLDER);
return createFolder(folder, path, session);
} else {
File file = this.resolve(path.getResourcePath(), FILE);
return createFile(file, path, session);
}
}
public DavFile createFile(File file, DavResourceLocator locator, DavSession session) {
return new DavFile(this, lockManager, session, locator, file);
DavFolder createFolder(Folder folder, DavPath path, DavSession session) {
return new DavFolder(this, lockManager, session, path, folder);
}
DavFile createFile(File file, DavPath path, DavSession session) {
return new DavFile(this, lockManager, session, path, file);
}
private <T extends Node> T resolve(String path, Class<T> expectedNodeType) {

View File

@@ -28,7 +28,7 @@ public class WebDavServlet extends AbstractWebdavServlet {
public WebDavServlet(URI contextRootUri, FileSystem filesystem) {
davSessionProvider = new DavSessionProviderImpl();
davLocatorFactory = new IdentityLocatorFactory(contextRootUri);
davLocatorFactory = new DavPathFactory(contextRootUri);
davResourceFactory = new FilesystemResourceFactory(filesystem);
}

View File

@@ -11,13 +11,18 @@ package org.cryptomator.webdav.jackrabbit;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.EnumSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.servlet.DispatcherType;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.FolderCreateMode;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.cryptomator.webdav.filters.AcceptRangeFilter;
import org.cryptomator.webdav.filters.UriNormalizationFilter;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
@@ -52,6 +57,8 @@ public class InMemoryWebDavServer {
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.SESSIONS);
final ServletHolder servletHolder = new ServletHolder("InMemory-WebDAV-Servlet", new WebDavServlet(servletContextRootUri, inMemoryFileSystem));
servletContext.addServlet(servletHolder, "/*");
servletContext.addFilter(AcceptRangeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
servletContext.addFilter(UriNormalizationFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
servletCollection.mapContexts();
server.setConnectors(new Connector[] {localConnector});