diff --git a/main/jackrabbit-filesystem-adapter/pom.xml b/main/jackrabbit-filesystem-adapter/pom.xml
new file mode 100644
index 000000000..8eed17c99
--- /dev/null
+++ b/main/jackrabbit-filesystem-adapter/pom.xml
@@ -0,0 +1,78 @@
+
+
+
+ 4.0.0
+
+ org.cryptomator
+ main
+ 0.11.0-SNAPSHOT
+
+ jackrabbit-filesystem-adapter
+ Jackrabbit filesystem-WebDAV-adapter
+ WebDAV servlet based on Apache Jackrabbit serving files from a filesystem
+
+
+ 2.11.0
+
+
+
+
+
+ org.cryptomator
+ filesystem-api
+
+
+
+
+ org.apache.jackrabbit
+ jackrabbit-webdav
+ ${jackrabbit.version}
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+
+ org.eclipse.jetty
+ jetty-server
+ 9.3.3.v20150827
+ test
+
+
+ org.eclipse.jetty
+ jetty-webapp
+ 9.3.3.v20150827
+ test
+
+
+ commons-httpclient
+ commons-httpclient
+ test
+
+
+ org.cryptomator
+ filesystem-inmemory
+ test
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+
\ No newline at end of file
diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFile.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFile.java
new file mode 100644
index 000000000..3eb96adfa
--- /dev/null
+++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFile.java
@@ -0,0 +1,98 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+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;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+
+class DavFile extends DavNode {
+
+ public DavFile(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavResourceLocator locator, File node) {
+ super(factory, lockManager, session, locator, node);
+ }
+
+ @Override
+ public boolean isCollection() {
+ return false;
+ }
+
+ @Override
+ public void spool(OutputContext outputContext) throws IOException {
+ outputContext.setModificationTime(node.lastModified().toEpochMilli());
+ if (!outputContext.hasStream()) {
+ return;
+ }
+ try (ReadableFile src = node.openReadable(); WritableByteChannel dst = Channels.newChannel(outputContext.getOutputStream())) {
+ // TODO filesize before sending content
+ outputContext.setContentLength(-1l);
+ ByteBuffer buf = ByteBuffer.allocate(1337);
+ do {
+ buf.clear();
+ src.read(buf);
+ buf.flip();
+ } while (dst.write(buf) > 0);
+ }
+ }
+
+ @Override
+ public void addMember(DavResource resource, InputContext inputContext) throws DavException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DavResourceIterator getMembers() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeMember(DavResource member) throws DavException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void move(DavResource destination) throws DavException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void copy(DavResource destination, boolean shallow) throws DavException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected void setModificationTime(Instant instant) {
+ try (WritableFile writable = node.openWritable()) {
+ writable.setLastModified(instant);
+ }
+ }
+
+ @Override
+ protected void setCreationTime(Instant instant) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFolder.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFolder.java
new file mode 100644
index 000000000..132092699
--- /dev/null
+++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFolder.java
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+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;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
+import org.apache.jackrabbit.webdav.property.ResourceType;
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.Folder;
+
+class DavFolder extends DavNode {
+
+ public DavFolder(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavResourceLocator locator, Folder folder) {
+ super(factory, lockManager, session, locator, folder);
+ properties.add(new ResourceType(ResourceType.COLLECTION));
+ properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1));
+ }
+
+ @Override
+ public boolean isCollection() {
+ return true;
+ }
+
+ @Override
+ public void spool(OutputContext outputContext) throws IOException {
+ // no-op
+ }
+
+ @Override
+ public void addMember(DavResource resource, InputContext inputContext) throws DavException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public DavResourceIterator getMembers() {
+ final Stream folders = node.folders().map(this::getMemberFolder);
+ final Stream files = node.files().map(this::getMemberFile);
+ return new DavResourceIteratorImpl(Stream.concat(folders, files).collect(Collectors.toList()));
+ }
+
+ private DavFolder getMemberFolder(Folder memberFolder) {
+ final String subFolderResourcePath = locator.getResourcePath() + memberFolder.name() + '/';
+ final DavResourceLocator subFolderLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), subFolderResourcePath);
+ 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);
+ return factory.createFile(memberFile, subFolderLocator, session);
+ }
+
+ @Override
+ public void removeMember(DavResource member) throws DavException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void move(DavResource destination) throws DavException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void copy(DavResource destination, boolean shallow) throws DavException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected void setModificationTime(Instant instant) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected void setCreationTime(Instant instant) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavNode.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavNode.java
new file mode 100644
index 000000000..340f0654d
--- /dev/null
+++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavNode.java
@@ -0,0 +1,228 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.io.UncheckedIOException;
+import java.time.Instant;
+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;
+import org.apache.jackrabbit.webdav.DavSession;
+import org.apache.jackrabbit.webdav.MultiStatusResponse;
+import org.apache.jackrabbit.webdav.lock.ActiveLock;
+import org.apache.jackrabbit.webdav.lock.LockInfo;
+import org.apache.jackrabbit.webdav.lock.LockManager;
+import org.apache.jackrabbit.webdav.lock.Scope;
+import org.apache.jackrabbit.webdav.lock.Type;
+import org.apache.jackrabbit.webdav.property.DavProperty;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+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;
+
+abstract class DavNode implements DavResource {
+
+ private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
+ private static final String[] DAV_CREATIONDATE_PROPNAMES = {DavPropertyName.CREATIONDATE.getName(), "Win32CreationTime"};
+ private static final String[] DAV_MODIFIEDDATE_PROPNAMES = {DavPropertyName.GETLASTMODIFIED.getName(), "Win32LastModifiedTime"};
+
+ protected final FilesystemResourceFactory factory;
+ protected final LockManager lockManager;
+ protected final DavSession session;
+ protected final DavResourceLocator locator;
+ protected final T node;
+ protected final DavPropertySet properties;
+
+ public DavNode(FilesystemResourceFactory factory, LockManager lockManager, DavSession session, DavResourceLocator locator, T node) {
+ this.factory = factory;
+ this.lockManager = lockManager;
+ this.session = session;
+ this.locator = locator;
+ this.node = node;
+ this.properties = new DavPropertySet();
+ }
+
+ @Override
+ public String getComplianceClass() {
+ return DAV_COMPLIANCE_CLASSES;
+ }
+
+ @Override
+ public String getSupportedMethods() {
+ return METHODS;
+ }
+
+ @Override
+ public boolean exists() {
+ return node.exists();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return node.name();
+ }
+
+ @Override
+ public DavResourceLocator getLocator() {
+ return locator;
+ }
+
+ @Override
+ public String getResourcePath() {
+ return locator.getResourcePath();
+ }
+
+ @Override
+ public String getHref() {
+ return locator.getHref(this.isCollection());
+ }
+
+ @Override
+ public long getModificationTime() {
+ try {
+ return node.lastModified().toEpochMilli();
+ } catch (UncheckedIOException e) {
+ return -1l;
+ }
+ }
+
+ protected abstract void setModificationTime(Instant instant);
+
+ protected abstract void setCreationTime(Instant instant);
+
+ @Override
+ public DavPropertyName[] getPropertyNames() {
+ return getProperties().getPropertyNames();
+ }
+
+ @Override
+ public DavProperty> getProperty(DavPropertyName name) {
+ return getProperties().get(name);
+ }
+
+ @Override
+ public DavPropertySet getProperties() {
+ return properties;
+ }
+
+ @Override
+ public void setProperty(DavProperty> property) throws DavException {
+ getProperties().add(property);
+
+ final String namespacelessPropertyName = property.getName().getName();
+ if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
+ final String createDateStr = (String) property.getValue();
+ final Instant createTime = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(createDateStr));
+ this.setCreationTime(createTime);
+ } else if (Arrays.asList(DAV_MODIFIEDDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
+ final String lastModifiedTimeStr = (String) property.getValue();
+ final Instant createTime = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(lastModifiedTimeStr));
+ this.setCreationTime(createTime);
+ }
+ }
+
+ @Override
+ public void removeProperty(DavPropertyName propertyName) throws DavException {
+ getProperties().remove(propertyName);
+ }
+
+ @Override
+ public MultiStatusResponse alterProperties(List extends PropEntry> changeList) throws DavException {
+ final DavPropertyNameSet names = new DavPropertyNameSet();
+ for (final PropEntry entry : changeList) {
+ if (entry instanceof DavProperty) {
+ final DavProperty> prop = (DavProperty>) entry;
+ this.setProperty(prop);
+ names.add(prop.getName());
+ } else if (entry instanceof DavPropertyName) {
+ final DavPropertyName name = (DavPropertyName) entry;
+ this.removeProperty(name);
+ names.add(name);
+ }
+ }
+ return new MultiStatusResponse(this, names);
+ }
+
+ @Override
+ public DavResource getCollection() {
+ if (locator.isRootLocation()) {
+ return null;
+ }
+
+ final String parentResource = FilenameUtils.getPathNoEndSeparator(locator.getResourcePath());
+ final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
+ try {
+ return factory.createResource(parentLocator, session);
+ } catch (DavException e) {
+ throw new IllegalStateException("Unable to get parent resource with path " + parentLocator.getResourcePath(), e);
+ }
+ }
+
+ @Override
+ public boolean isLockable(Type type, Scope scope) {
+ return true;
+ }
+
+ @Override
+ public boolean hasLock(Type type, Scope scope) {
+ return getLock(type, scope) != null;
+ }
+
+ @Override
+ public ActiveLock getLock(Type type, Scope scope) {
+ return lockManager.getLock(type, scope, this);
+ }
+
+ @Override
+ public ActiveLock[] getLocks() {
+ final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
+ if (exclusiveWriteLock != null) {
+ return new ActiveLock[] {exclusiveWriteLock};
+ } else {
+ return new ActiveLock[0];
+ }
+ }
+
+ @Override
+ public ActiveLock lock(LockInfo reqLockInfo) throws DavException {
+ return lockManager.createLock(reqLockInfo, this);
+ }
+
+ @Override
+ public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) throws DavException {
+ return lockManager.refreshLock(reqLockInfo, lockToken, this);
+ }
+
+ @Override
+ public void unlock(String lockToken) throws DavException {
+ lockManager.releaseLock(lockToken, this);
+ }
+
+ @Override
+ public void addLockManager(LockManager lockmgr) {
+ throw new UnsupportedOperationException("Locks are managed");
+ }
+
+ @Override
+ public FilesystemResourceFactory getFactory() {
+ return factory;
+ }
+
+ @Override
+ public DavSession getSession() {
+ return session;
+ }
+
+}
diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavSessionImpl.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavSessionImpl.java
new file mode 100644
index 000000000..61b045a29
--- /dev/null
+++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavSessionImpl.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Copyright (c) 2014, 2015 Sebastian Stenzel
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ ******************************************************************************/
+package org.cryptomator.webdav.jackrabbit;
+
+import java.util.HashSet;
+
+import org.apache.jackrabbit.webdav.DavSession;
+
+class DavSessionImpl implements DavSession {
+
+ private final HashSet lockTokens = new HashSet();
+ private final HashSet