Browse Source

yet another refactoring session (functionality restored now)

Sebastian Stenzel 10 years ago
parent
commit
ea9c8eee83

+ 7 - 8
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractEncryptedNode.java

@@ -48,19 +48,19 @@ abstract class AbstractEncryptedNode implements DavResource {
 	protected final DavSession session;
 	protected final LockManager lockManager;
 	protected final Cryptor cryptor;
+	protected final Path filePath;
 	protected final DavPropertySet properties;
 
-	protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
+	protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath) {
 		this.factory = factory;
 		this.locator = locator;
 		this.session = session;
 		this.lockManager = lockManager;
 		this.cryptor = cryptor;
+		this.filePath = filePath;
 		this.properties = new DavPropertySet();
 	}
 
-	protected abstract Path getPhysicalPath();
-
 	@Override
 	public String getComplianceClass() {
 		return DAV_COMPLIANCE_CLASSES;
@@ -73,7 +73,7 @@ abstract class AbstractEncryptedNode implements DavResource {
 
 	@Override
 	public boolean exists() {
-		return Files.exists(getPhysicalPath());
+		return Files.exists(filePath);
 	}
 
 	@Override
@@ -105,7 +105,7 @@ abstract class AbstractEncryptedNode implements DavResource {
 	@Override
 	public long getModificationTime() {
 		try {
-			return Files.getLastModifiedTime(getPhysicalPath()).toMillis();
+			return Files.getLastModifiedTime(filePath).toMillis();
 		} catch (IOException e) {
 			return -1;
 		}
@@ -133,17 +133,16 @@ abstract class AbstractEncryptedNode implements DavResource {
 		LOG.info("Set property {}", property.getName());
 
 		try {
-			final Path path = getPhysicalPath();
 			if (DavPropertyName.CREATIONDATE.equals(property.getName()) && property.getValue() instanceof String) {
 				final String createDateStr = (String) property.getValue();
 				final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
-				final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
+				final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
 				attrView.setTimes(null, null, createTime);
 				LOG.info("Updating Creation Date: {}", createTime.toString());
 			} else if (DavPropertyName.GETLASTMODIFIED.equals(property.getName()) && property.getValue() instanceof String) {
 				final String lastModifiedTimeStr = (String) property.getValue();
 				final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
-				final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
+				final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
 				attrView.setTimes(lastModifiedTime, null, null);
 				LOG.info("Updating Last Modified Date: {}", lastModifiedTime.toString());
 			}

+ 39 - 37
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java

@@ -5,6 +5,7 @@ import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -48,61 +49,46 @@ public class CryptoResourceFactory implements DavResourceFactory, FileNamingConv
 
 	@Override
 	public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
-		if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
-			final String parentResourcePath = FilenameUtils.getFullPathNoEndSeparator(locator.getResourcePath());
-			final Path parentDirectoryPath = createEncryptedDirectoryPath(parentResourcePath);
-			return new EncryptedDirDuringCreation(this, locator, request.getDavSession(), lockManager, cryptor, filenameTranslator, parentDirectoryPath);
-		}
-
 		if (locator.isRootLocation()) {
-			final Path dirpath = createEncryptedDirectoryPath("");
-			return createDirectory(locator, request.getDavSession(), dirpath);
+			return createRootDirectory(locator, request.getDavSession());
 		}
 
-		final Path filepath = getEncryptedFilePath(locator.getResourcePath());
+		final Path filePath = getEncryptedFilePath(locator.getResourcePath());
 		final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
 		final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
-		if (Files.exists(dirFilePath)) {
-			final Path dirPath = createEncryptedDirectoryPath(locator.getResourcePath());
-			return createDirectory(locator, request.getDavSession(), dirPath);
-		} else if (Files.exists(filepath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
+		if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
+			return createDirectory(locator, request.getDavSession(), dirFilePath);
+		} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
 			response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
-			return createFilePart(locator, request.getDavSession(), request, filepath);
-		} else if (Files.exists(filepath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
-			return createFile(locator, request.getDavSession(), filepath);
+			return createFilePart(locator, request.getDavSession(), request, filePath);
+		} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
+			return createFile(locator, request.getDavSession(), filePath);
 		} else {
-			return createNonExisting(locator, request.getDavSession());
+			// e.g. for MOVE operations:
+			return createNonExisting(locator, request.getDavSession(), filePath, dirFilePath);
 		}
 	}
 
 	@Override
 	public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
 		if (locator.isRootLocation()) {
-			final Path dirpath = createEncryptedDirectoryPath("");
-			return createDirectory(locator, session, dirpath);
+			return createRootDirectory(locator, session);
 		}
 
-		final Path filepath = getEncryptedFilePath(locator.getResourcePath());
+		final Path filePath = getEncryptedFilePath(locator.getResourcePath());
 		final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
 		if (Files.exists(dirFilePath)) {
-			final Path dirPath = createEncryptedDirectoryPath(locator.getResourcePath());
-			return createDirectory(locator, session, dirPath);
-		} else if (Files.exists(filepath)) {
-			return createFile(locator, session, filepath);
+			return createDirectory(locator, session, dirFilePath);
+		} else if (Files.exists(filePath)) {
+			return createFile(locator, session, filePath);
 		} else {
-			return createNonExisting(locator, session);
+			// e.g. for MOVE operations:
+			return createNonExisting(locator, session, filePath, dirFilePath);
 		}
 	}
 
 	DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException {
-		try {
-			final String directoryId = new String(readAllBytesAtomically(existingDirectoryFile), StandardCharsets.UTF_8);
-			final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
-			final Path dirpath = dataRoot.resolve(directory);
-			return createDirectory(locator, session, dirpath);
-		} catch (IOException e) {
-			throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
-		}
+		return createDirectory(locator, session, existingDirectoryFile);
 	}
 
 	DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException {
@@ -184,12 +170,28 @@ public class CryptoResourceFactory implements DavResourceFactory, FileNamingConv
 		return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
 	}
 
-	private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path dirPath) {
-		return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, dirPath);
+	private EncryptedDir createRootDirectory(DavResourceLocator locator, DavSession session) throws DavException {
+		final Path rootFile = dataRoot.resolve(ROOT_FILE);
+		final Path rootDir = filenameTranslator.getEncryptedDirectoryPath("");
+		try {
+			// make sure, root dir always exists.
+			// create dir first (because it fails silently, if alreay existing)
+			Files.createDirectories(rootDir);
+			Files.createFile(rootFile);
+		} catch (FileAlreadyExistsException e) {
+			// no-op
+		} catch (IOException e) {
+			throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
+		}
+		return createDirectory(locator, session, dataRoot.resolve(ROOT_FILE));
+	}
+
+	private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path filePath) {
+		return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath);
 	}
 
-	private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
-		return new NonExistingNode(this, locator, session, lockManager, cryptor);
+	private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session, Path filePath, Path dirFilePath) {
+		return new NonExistingNode(this, locator, session, lockManager, cryptor, filePath, dirFilePath);
 	}
 
 	/* IO support */

+ 142 - 67
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java

@@ -15,9 +15,11 @@ import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.nio.channels.SeekableByteChannel;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.AtomicMoveNotSupportedException;
 import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
@@ -27,6 +29,7 @@ import java.util.UUID;
 
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.jackrabbit.webdav.DavException;
 import org.apache.jackrabbit.webdav.DavResource;
 import org.apache.jackrabbit.webdav.DavResourceIterator;
@@ -53,39 +56,61 @@ import org.slf4j.LoggerFactory;
 class EncryptedDir extends AbstractEncryptedNode implements FileNamingConventions {
 
 	private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
-	private final Path directoryPath;
 	private final FilenameTranslator filenameTranslator;
+	private String directoryId;
+	private Path directoryPath;
 
-	public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path directoryPath) {
-		super(factory, locator, session, lockManager, cryptor);
-		if (directoryPath == null || !Files.isDirectory(directoryPath)) {
-			throw new IllegalArgumentException("directoryPath must be an existing directory, but was " + directoryPath);
-		}
-		this.directoryPath = directoryPath;
+	public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path filePath) {
+		super(factory, locator, session, lockManager, cryptor, filePath);
 		this.filenameTranslator = filenameTranslator;
 		determineProperties();
 	}
 
-	@Override
-	protected Path getPhysicalPath() {
-		return directoryPath;
+	/**
+	 * @return Path or <code>null</code>, if directory does not yet exist.
+	 */
+	protected synchronized String getDirectoryId() {
+		if (directoryId == null) {
+			try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
+				final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
+				c.read(buffer);
+				directoryId = new String(buffer.array(), StandardCharsets.UTF_8);
+			} catch (FileNotFoundException e) {
+				directoryId = null;
+			} catch (IOException e) {
+				throw new IORuntimeException(e);
+			}
+		}
+		return directoryId;
 	}
 
-	@Override
-	public boolean isCollection() {
-		return true;
+	/**
+	 * @return Path or <code>null</code>, if directory does not yet exist.
+	 */
+	private synchronized Path getDirectoryPath() {
+		if (directoryPath == null) {
+			final String dirId = getDirectoryId();
+			if (dirId != null) {
+				directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
+			}
+		}
+		return directoryPath;
 	}
 
 	@Override
-	public boolean exists() {
-		assert Files.isDirectory(directoryPath);
+	public boolean isCollection() {
 		return true;
 	}
 
 	@Override
 	public long getModificationTime() {
 		try {
-			return Files.getLastModifiedTime(directoryPath).toMillis();
+			final Path dirPath = getDirectoryPath();
+			if (dirPath == null) {
+				return -1;
+			} else {
+				return Files.getLastModifiedTime(dirPath).toMillis();
+			}
 		} catch (IOException e) {
 			return -1;
 		}
@@ -108,13 +133,15 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
 		}
 	}
 
-	@Deprecated
 	private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
-		LOG.warn("Invokation of addMemberDir(DavResourceLocator childLocator, InputContext inputContext)");
+		final Path dirPath = getDirectoryPath();
+		if (dirPath == null) {
+			throw new DavException(DavServletResponse.SC_NOT_FOUND);
+		}
 		try {
 			final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath());
 			final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName);
-			final Path dirFilePath = directoryPath.resolve(ciphertextDirName);
+			final Path dirFilePath = dirPath.resolve(ciphertextDirName);
 			final String directoryId;
 			if (Files.exists(dirFilePath)) {
 				try (final FileChannel c = FileChannel.open(dirFilePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
@@ -138,10 +165,14 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
 	}
 
 	private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
+		final Path dirPath = getDirectoryPath();
+		if (dirPath == null) {
+			throw new DavException(DavServletResponse.SC_NOT_FOUND);
+		}
 		try {
 			final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
 			final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
-			final Path filePath = directoryPath.resolve(ciphertextFilename);
+			final Path filePath = dirPath.resolve(ciphertextFilename);
 			try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
 				cryptor.encryptFile(inputContext.getInputStream(), channel);
 			} catch (SecurityException e) {
@@ -164,7 +195,11 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
 	@Override
 	public DavResourceIterator getMembers() {
 		try {
-			final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directoryPath, DIRECTORY_CONTENT_FILTER);
+			final Path dirPath = getDirectoryPath();
+			if (dirPath == null) {
+				throw new DavException(DavServletResponse.SC_NOT_FOUND);
+			}
+			final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dirPath, DIRECTORY_CONTENT_FILTER);
 			final List<DavResource> result = new ArrayList<>();
 
 			for (final Path childPath : directoryStream) {
@@ -205,6 +240,10 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
 	}
 
 	private void removeMember(AbstractEncryptedNode member) throws DavException {
+		final Path dirPath = getDirectoryPath();
+		if (dirPath == null) {
+			throw new DavException(DavServletResponse.SC_NOT_FOUND);
+		}
 		try {
 			final String cleartextFilename = FilenameUtils.getName(member.getResourcePath());
 			final String ciphertextFilename;
@@ -215,12 +254,15 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
 					DavResource m = iterator.next();
 					member.removeMember(m);
 				}
-				Files.deleteIfExists(subDir.directoryPath);
+				final Path subDirPath = subDir.getDirectoryPath();
+				if (subDirPath != null) {
+					Files.deleteIfExists(subDirPath);
+				}
 				ciphertextFilename = filenameTranslator.getEncryptedDirName(cleartextFilename);
 			} else {
 				ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
 			}
-			final Path memberPath = directoryPath.resolve(ciphertextFilename);
+			final Path memberPath = dirPath.resolve(ciphertextFilename);
 			Files.deleteIfExists(memberPath);
 		} catch (FileNotFoundException e) {
 			// no-op
@@ -231,50 +273,81 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
 
 	@Override
 	public void move(AbstractEncryptedNode dest) throws DavException, IOException {
-		throw new UnsupportedOperationException("not yet implemented");
-		// final Path srcDir = this.locator.getEncryptedDirectoryPath(false);
-		// final Path dstDir = dest.locator.getEncryptedDirectoryPath(true);
-		// final Path srcFile = this.locator.getEncryptedFilePath();
-		// final Path dstFile = dest.locator.getEncryptedFilePath();
-		//
-		// // check for conflicts:
-		// if (Files.exists(dstDir) && Files.getLastModifiedTime(dstDir).toMillis() > Files.getLastModifiedTime(dstDir).toMillis()) {
-		// throw new DavException(DavServletResponse.SC_CONFLICT, "Directory at destination already exists: " + dstDir.toString());
-		// }
-		//
-		// // move:
-		// Files.createDirectories(dstDir);
-		// try {
-		// Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
-		// Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
-		// } catch (AtomicMoveNotSupportedException e) {
-		// Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING);
-		// Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING);
-		// }
+		// when moving a directory we only need to move the file (actual dir is ID-dependent and won't change)
+		final Path srcPath = filePath;
+		final Path dstPath;
+		if (dest instanceof NonExistingNode) {
+			dstPath = ((NonExistingNode) dest).getDirFilePath();
+		} else {
+			dstPath = dest.filePath;
+		}
+
+		// move:
+		Files.createDirectories(dstPath.getParent());
+		try {
+			Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+		} catch (AtomicMoveNotSupportedException e) {
+			Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
+		}
 	}
 
 	@Override
 	public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
-		throw new UnsupportedOperationException("not yet implemented");
-		// final Path srcDir = this.locator.getEncryptedDirectoryPath(false);
-		// final Path dstDir = dest.locator.getEncryptedDirectoryPath(true);
-		// final Path srcFile = this.locator.getEncryptedFilePath();
-		// final Path dstFile = dest.locator.getEncryptedFilePath();
-		//
-		// // check for conflicts:
-		// if (Files.exists(dstDir) && Files.getLastModifiedTime(dstDir).toMillis() > Files.getLastModifiedTime(dstDir).toMillis()) {
-		// throw new DavException(DavServletResponse.SC_CONFLICT, "Directory at destination already exists: " + dstDir.toString());
-		// }
-		//
-		// // copy:
-		// Files.createDirectories(dstDir);
-		// try {
-		// Files.copy(srcDir, dstDir, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
-		// Files.copy(srcFile, dstFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
-		// } catch (AtomicMoveNotSupportedException e) {
-		// Files.copy(srcDir, dstDir, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
-		// Files.copy(srcFile, dstFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
-		// }
+		final Path dstDirFilePath;
+		if (dest instanceof NonExistingNode) {
+			dstDirFilePath = ((NonExistingNode) dest).getDirFilePath();
+		} else {
+			dstDirFilePath = dest.filePath;
+		}
+
+		// copy dirFile:
+		final String srcDirId = getDirectoryId();
+		if (srcDirId == null) {
+			throw new DavException(DavServletResponse.SC_NOT_FOUND);
+		}
+		final String dstDirId = UUID.randomUUID().toString();
+		try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
+			c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8)));
+		}
+
+		// copy actual dir:
+		if (!shallow) {
+			copyDirectoryContents(srcDirId, dstDirId);
+		} else {
+			final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
+			Files.createDirectories(dstDirPath);
+		}
+	}
+
+	private void copyDirectoryContents(String srcDirId, String dstDirId) throws IOException {
+		final Path srcDirPath = filenameTranslator.getEncryptedDirectoryPath(srcDirId);
+		final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
+		Files.createDirectories(dstDirPath);
+		final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(srcDirPath, DIRECTORY_CONTENT_FILTER);
+		for (final Path srcChildPath : directoryStream) {
+			final String childName = srcChildPath.getFileName().toString();
+			final Path dstChildPath = dstDirPath.resolve(childName);
+			if (StringUtils.endsWithIgnoreCase(childName, FILE_EXT)) {
+				try {
+					Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+				} catch (AtomicMoveNotSupportedException e) {
+					Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
+				}
+			} else if (StringUtils.endsWithIgnoreCase(childName, DIR_EXT)) {
+				final String srcSubdirId;
+				try (final FileChannel c = FileChannel.open(srcChildPath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
+					final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
+					c.read(buffer);
+					srcSubdirId = new String(buffer.array(), StandardCharsets.UTF_8);
+				}
+				final String dstSubdirId = UUID.randomUUID().toString();
+				try (final FileChannel c = FileChannel.open(dstChildPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
+						final FileLock lock = c.lock()) {
+					c.write(ByteBuffer.wrap(dstSubdirId.getBytes(StandardCharsets.UTF_8)));
+				}
+				copyDirectoryContents(srcSubdirId, dstSubdirId);
+			}
+		}
 	}
 
 	@Override
@@ -287,11 +360,13 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
 		properties.add(new ResourceType(ResourceType.COLLECTION));
 		properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
 		try {
-			final BasicFileAttributes attrs = Files.readAttributes(directoryPath, BasicFileAttributes.class);
-			properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
-			properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
+			if (Files.exists(filePath)) {
+				final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
+				properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
+				properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
+			}
 		} catch (IOException e) {
-			LOG.error("Error determining metadata " + directoryPath.toString(), e);
+			LOG.error("Error determining metadata " + filePath, e);
 			// don't add any further properties
 		}
 	}

+ 0 - 114
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDirDuringCreation.java

@@ -1,114 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2014 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.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.channels.FileLock;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.time.Instant;
-import java.util.UUID;
-
-import org.apache.commons.io.FilenameUtils;
-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.DavServletResponse;
-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.crypto.Cryptor;
-
-class EncryptedDirDuringCreation extends AbstractEncryptedNode {
-
-	private final Path parentDir;
-	private final FilenameTranslator filenameTranslator;
-
-	public EncryptedDirDuringCreation(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path parentDir) {
-		super(factory, locator, session, lockManager, cryptor);
-		this.parentDir = parentDir;
-		this.filenameTranslator = filenameTranslator;
-	}
-
-	public void doCreate() throws DavException {
-		try {
-			final String cleartextDirName = FilenameUtils.getName(locator.getResourcePath());
-			final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName);
-			final Path dirFilePath = parentDir.resolve(ciphertextDirName);
-			final String directoryId = UUID.randomUUID().toString();
-			try (final FileChannel c = FileChannel.open(dirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
-				c.write(ByteBuffer.wrap(directoryId.getBytes(StandardCharsets.UTF_8)));
-			} catch (FileAlreadyExistsException e) {
-				throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED);
-			}
-			final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
-			Files.createDirectories(directoryPath);
-		} catch (IOException e) {
-			throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
-		}
-	}
-
-	@Override
-	protected Path getPhysicalPath() {
-		throw new UnsupportedOperationException("Resource doesn't exist.");
-	}
-
-	@Override
-	public boolean exists() {
-		return false;
-	}
-
-	@Override
-	public boolean isCollection() {
-		return true;
-	}
-
-	@Override
-	public long getModificationTime() {
-		return Instant.now().toEpochMilli();
-	}
-
-	@Override
-	public void spool(OutputContext outputContext) throws IOException {
-		throw new UnsupportedOperationException("Resource doesn't exist.");
-	}
-
-	@Override
-	public void addMember(DavResource resource, InputContext inputContext) throws DavException {
-		throw new UnsupportedOperationException("Resource doesn't exist.");
-	}
-
-	@Override
-	public DavResourceIterator getMembers() {
-		throw new UnsupportedOperationException("Resource doesn't exist.");
-	}
-
-	@Override
-	public void removeMember(DavResource member) throws DavException {
-		throw new UnsupportedOperationException("Resource doesn't exist.");
-	}
-
-	@Override
-	public void move(AbstractEncryptedNode destination) throws DavException {
-		throw new UnsupportedOperationException("Resource doesn't exist.");
-	}
-
-	@Override
-	public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException {
-		throw new UnsupportedOperationException("Resource doesn't exist.");
-	}
-
-}

+ 29 - 38
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java

@@ -11,8 +11,10 @@ package org.cryptomator.webdav.jackrabbit;
 import java.io.EOFException;
 import java.io.IOException;
 import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AtomicMoveNotSupportedException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.BasicFileAttributes;
 
@@ -40,23 +42,16 @@ class EncryptedFile extends AbstractEncryptedNode {
 	private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
 
 	protected final CryptoWarningHandler cryptoWarningHandler;
-	protected final Path filePath;
 
 	public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) {
-		super(factory, locator, session, lockManager, cryptor);
+		super(factory, locator, session, lockManager, cryptor, filePath);
 		if (filePath == null) {
 			throw new IllegalArgumentException("filePath must not be null");
 		}
 		this.cryptoWarningHandler = cryptoWarningHandler;
-		this.filePath = filePath;
 		this.determineProperties();
 	}
 
-	@Override
-	protected Path getPhysicalPath() {
-		return filePath;
-	}
-
 	@Override
 	public boolean isCollection() {
 		return false;
@@ -128,40 +123,36 @@ class EncryptedFile extends AbstractEncryptedNode {
 
 	@Override
 	public void move(AbstractEncryptedNode dest) throws DavException, IOException {
-		throw new UnsupportedOperationException("not yet implemented");
-		// final Path src = this.locator.getEncryptedFilePath();
-		// final Path dst = dest.locator.getEncryptedFilePath();
-		//
-		// // check for conflicts:
-		// if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
-		// throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
-		// }
-		//
-		// // move:
-		// try {
-		// Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
-		// } catch (AtomicMoveNotSupportedException e) {
-		// Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
-		// }
+		final Path srcPath = filePath;
+		final Path dstPath;
+		if (dest instanceof NonExistingNode) {
+			dstPath = ((NonExistingNode) dest).getFilePath();
+		} else {
+			dstPath = dest.filePath;
+		}
+
+		try {
+			Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+		} catch (AtomicMoveNotSupportedException e) {
+			Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
+		}
 	}
 
 	@Override
 	public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
-		throw new UnsupportedOperationException("not yet implemented");
-		// final Path src = this.locator.getEncryptedFilePath();
-		// final Path dst = dest.locator.getEncryptedFilePath();
-		//
-		// // check for conflicts:
-		// if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
-		// throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
-		// }
-		//
-		// // copy:
-		// try {
-		// Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
-		// } catch (AtomicMoveNotSupportedException e) {
-		// Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
-		// }
+		final Path srcPath = filePath;
+		final Path dstPath;
+		if (dest instanceof NonExistingNode) {
+			dstPath = ((NonExistingNode) dest).getFilePath();
+		} else {
+			dstPath = dest.filePath;
+		}
+
+		try {
+			Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+		} catch (AtomicMoveNotSupportedException e) {
+			Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
+		}
 	}
 
 }

+ 7 - 2
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FileNamingConventions.java

@@ -21,9 +21,14 @@ interface FileNamingConventions {
 	/**
 	 * Maximum path length on some file systems or cloud storage providers is restricted.<br/>
 	 * Parent folder path uses up to 58 chars (sha256 -&gt; 32 bytes base32 encoded to 56 bytes + two slashes). That in mind we don't want the total path to be longer than 255 chars.<br/>
-	 * 128 chars would be enought for up to 80 plaintext chars. Also we need up to 8 chars for our file extension. So lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
+	 * 128 chars would be enought for up to 80 plaintext chars. Also we need up to 9 chars for our file extension. So lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
 	 */
-	int ENCRYPTED_FILENAME_LENGTH_LIMIT = 136;
+	int ENCRYPTED_FILENAME_LENGTH_LIMIT = 137;
+
+	/**
+	 * Dummy file, on which file attributes can be stored for the root directory.
+	 */
+	String ROOT_FILE = "root";
 
 	/**
 	 * For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.

+ 20 - 6
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/NonExistingNode.java

@@ -19,17 +19,18 @@ 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.DavProperty;
 import org.cryptomator.crypto.Cryptor;
 
 class NonExistingNode extends AbstractEncryptedNode {
 
-	public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
-		super(factory, locator, session, lockManager, cryptor);
-	}
+	private final Path filePath;
+	private final Path dirFilePath;
 
-	@Override
-	protected Path getPhysicalPath() {
-		throw new UnsupportedOperationException("Resource doesn't exist.");
+	public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath, Path dirFilePath) {
+		super(factory, locator, session, lockManager, cryptor, null);
+		this.filePath = filePath;
+		this.dirFilePath = dirFilePath;
 	}
 
 	@Override
@@ -77,4 +78,17 @@ class NonExistingNode extends AbstractEncryptedNode {
 		throw new UnsupportedOperationException("Resource doesn't exist.");
 	}
 
+	@Override
+	public void setProperty(DavProperty<?> property) throws DavException {
+		throw new UnsupportedOperationException("Resource doesn't exist.");
+	}
+
+	public Path getFilePath() {
+		return filePath;
+	}
+
+	public Path getDirFilePath() {
+		return dirFilePath;
+	}
+
 }

+ 10 - 14
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java

@@ -8,7 +8,6 @@
  ******************************************************************************/
 package org.cryptomator.webdav.jackrabbit;
 
-import java.io.IOException;
 import java.util.Collection;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -17,14 +16,11 @@ import java.util.concurrent.TimeUnit;
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 
-import org.apache.jackrabbit.webdav.DavException;
 import org.apache.jackrabbit.webdav.DavLocatorFactory;
 import org.apache.jackrabbit.webdav.DavResource;
 import org.apache.jackrabbit.webdav.DavResourceFactory;
-import org.apache.jackrabbit.webdav.DavServletResponse;
 import org.apache.jackrabbit.webdav.DavSessionProvider;
 import org.apache.jackrabbit.webdav.WebdavRequest;
-import org.apache.jackrabbit.webdav.WebdavResponse;
 import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
 import org.cryptomator.crypto.Cryptor;
 
@@ -71,16 +67,16 @@ public class WebDavServlet extends AbstractWebdavServlet {
 		}
 	}
 
-	@Override
-	protected void doMkCol(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
-		if (resource instanceof EncryptedDirDuringCreation) {
-			EncryptedDirDuringCreation dir = (EncryptedDirDuringCreation) resource;
-			dir.doCreate();
-			response.setStatus(DavServletResponse.SC_CREATED);
-		} else {
-
-		}
-	}
+	// @Override
+	// protected void doMkCol(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
+	// if (resource instanceof EncryptedDirDuringCreation) {
+	// EncryptedDirDuringCreation dir = (EncryptedDirDuringCreation) resource;
+	// dir.doCreate();
+	// response.setStatus(DavServletResponse.SC_CREATED);
+	// } else {
+	//
+	// }
+	// }
 
 	@Override
 	protected boolean isPreconditionValid(WebdavRequest request, DavResource resource) {