Explorar el Código

adjusted to updated API, restored Folder.copy and Folder.move

Sebastian Stenzel hace 9 años
padre
commit
762f362784

+ 7 - 3
main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFile.java

@@ -8,7 +8,6 @@
  *******************************************************************************/
 package org.cryptomator.crypto.fs;
 
-import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.time.Instant;
 import java.util.concurrent.TimeUnit;
@@ -39,15 +38,20 @@ public class CryptoFile extends CryptoNode implements File {
 	}
 
 	@Override
-	public ReadableFile openReadable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
+	public ReadableFile openReadable(long timeout, TimeUnit unit) throws TimeoutException {
 		// TODO Auto-generated method stub
 		return null;
 	}
 
 	@Override
-	public WritableFile openWritable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
+	public WritableFile openWritable(long timeout, TimeUnit unit) throws TimeoutException {
 		// TODO Auto-generated method stub
 		return null;
 	}
 
+	@Override
+	public String toString() {
+		return parent.toString() + name;
+	}
+
 }

+ 9 - 8
main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFileSystem.java

@@ -9,6 +9,7 @@
 package org.cryptomator.crypto.fs;
 
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
@@ -37,7 +38,7 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
 	}
 
 	@Override
-	File physicalFile() throws IOException {
+	File physicalFile() {
 		return physicalDataRoot().file(ROOT_DIR_FILE);
 	}
 
@@ -67,12 +68,7 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
 	}
 
 	@Override
-	public String toString() {
-		return "/";
-	}
-
-	@Override
-	public void create(FolderCreateMode mode) throws IOException {
+	public void create(FolderCreateMode mode) {
 		physicalDataRoot().create(mode);
 		physicalMetadataRoot().create(mode);
 		final File dirFile = physicalFile();
@@ -81,9 +77,14 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
 			final ByteBuffer buf = ByteBuffer.wrap(directoryId.getBytes());
 			writable.write(buf);
 		} catch (TimeoutException e) {
-			throw new IOException("Failed to lock directory file in time." + dirFile, e);
+			throw new UncheckedIOException(new IOException("Failed to lock directory file in time." + dirFile, e));
 		}
 		physicalFolder().create(FolderCreateMode.INCLUDING_PARENTS);
 	}
 
+	@Override
+	public String toString() {
+		return physicalRoot + ":::/";
+	}
+
 }

+ 63 - 18
main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFolder.java

@@ -15,6 +15,7 @@ import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
+import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -46,7 +47,7 @@ class CryptoFolder extends CryptoNode implements Folder {
 		return name() + FILE_EXT;
 	}
 
-	protected String getDirectoryId() throws IOException {
+	protected String getDirectoryId() {
 		if (directoryId.get() == null) {
 			File dirFile = physicalFile();
 			if (dirFile.exists()) {
@@ -58,9 +59,7 @@ class CryptoFolder extends CryptoNode implements Folder {
 					buf.get(bytes);
 					directoryId.set(new String(bytes));
 				} catch (TimeoutException e) {
-					throw new IOException("Failed to lock directory file in time." + dirFile, e);
-				} catch (IOException e) {
-					throw new UncheckedIOException(e);
+					throw new UncheckedIOException(new IOException("Failed to lock directory file in time." + dirFile, e));
 				}
 			} else {
 				directoryId.compareAndSet(null, UUID.randomUUID().toString());
@@ -69,11 +68,11 @@ class CryptoFolder extends CryptoNode implements Folder {
 		return directoryId.get();
 	}
 
-	File physicalFile() throws IOException {
+	File physicalFile() {
 		return parent.physicalFolder().file(encryptedName());
 	}
 
-	Folder physicalFolder() throws IOException {
+	Folder physicalFolder() {
 		final String encryptedThenHashedDirId;
 		try {
 			final byte[] hash = MessageDigest.getInstance("SHA-1").digest(getDirectoryId().getBytes());
@@ -87,20 +86,16 @@ class CryptoFolder extends CryptoNode implements Folder {
 
 	@Override
 	public Instant lastModified() {
-		try {
-			return physicalFile().lastModified();
-		} catch (IOException e) {
-			throw new UncheckedIOException(e);
-		}
+		return physicalFile().lastModified();
 	}
 
 	@Override
-	public Stream<? extends Node> children() throws IOException {
+	public Stream<? extends Node> children() {
 		return Stream.concat(files(), folders());
 	}
 
 	@Override
-	public Stream<CryptoFile> files() throws IOException {
+	public Stream<CryptoFile> files() {
 		return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFile.FILE_EXT)).map(this::decryptFileName).map(this::file);
 	}
 
@@ -115,7 +110,7 @@ class CryptoFolder extends CryptoNode implements Folder {
 	}
 
 	@Override
-	public Stream<CryptoFolder> folders() throws IOException {
+	public Stream<CryptoFolder> folders() {
 		return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFolder.FILE_EXT)).map(this::decryptFolderName).map(this::folder);
 	}
 
@@ -130,13 +125,13 @@ class CryptoFolder extends CryptoNode implements Folder {
 	}
 
 	@Override
-	public void create(FolderCreateMode mode) throws IOException {
+	public void create(FolderCreateMode mode) {
 		final File dirFile = physicalFile();
 		if (dirFile.exists()) {
 			return;
 		}
 		if (!parent.exists() && FolderCreateMode.FAIL_IF_PARENT_IS_MISSING.equals(mode)) {
-			throw new FileNotFoundException(parent.name);
+			throw new UncheckedIOException(new FileNotFoundException(parent.name));
 		} else if (!parent.exists() && FolderCreateMode.INCLUDING_PARENTS.equals(mode)) {
 			parent.create(mode);
 		}
@@ -146,15 +141,65 @@ class CryptoFolder extends CryptoNode implements Folder {
 			final ByteBuffer buf = ByteBuffer.wrap(directoryId.getBytes());
 			writable.write(buf);
 		} catch (TimeoutException e) {
-			throw new IOException("Failed to lock directory file in time." + dirFile, e);
+			throw new UncheckedIOException(new IOException("Failed to lock directory file in time." + dirFile, e));
 		}
 		physicalFolder().create(FolderCreateMode.INCLUDING_PARENTS);
 	}
 
 	@Override
-	public void delete() throws IOException {
+	public void copyTo(Folder target) {
+		if (this.contains(target)) {
+			throw new IllegalArgumentException("Can not copy parent to child directory (src: " + this + ", dst: " + target + ")");
+		}
+
+		Folder.super.copyTo(target);
+	}
+
+	@Override
+	public void moveTo(Folder target) {
+		if (target instanceof CryptoFolder) {
+			moveToInternal((CryptoFolder) target);
+		} else {
+			throw new UnsupportedOperationException("Can not move CryptoFolder to conventional folder.");
+		}
+	}
+
+	private void moveToInternal(CryptoFolder target) {
+		if (this.contains(target) || target.contains(this)) {
+			throw new IllegalArgumentException("Can not move directories containing one another (src: " + this + ", dst: " + target + ")");
+		}
+
+		target.physicalFile().parent().get().create(FolderCreateMode.INCLUDING_PARENTS);
+		assert target.physicalFile().parent().get().exists();
+		try (WritableFile src = this.physicalFile().openWritable(1, TimeUnit.SECONDS); WritableFile dst = target.physicalFile().openWritable(1, TimeUnit.SECONDS)) {
+			src.moveTo(dst);
+		} catch (TimeoutException e) {
+			throw new UncheckedIOException(new IOException("Failed to lock file for moving (src: " + this + ", dst: " + target + ")", e));
+		}
+		// directoryId is now used by target, we must no longer use the same id (we'll generate a new one when needed)
+		directoryId.set(null);
+	}
+
+	private boolean contains(Node node) {
+		Optional<? extends Folder> nodeParent = node.parent();
+		while (nodeParent.isPresent()) {
+			if (this.equals(nodeParent.get())) {
+				return true;
+			}
+			nodeParent = nodeParent.get().parent();
+		}
+		return false;
+	}
+
+	@Override
+	public void delete() {
 		// TODO Auto-generated method stub
 
 	}
 
+	@Override
+	public String toString() {
+		return parent.toString() + name + "/";
+	}
+
 }

+ 1 - 7
main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoNode.java

@@ -8,8 +8,6 @@
  *******************************************************************************/
 package org.cryptomator.crypto.fs;
 
-import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.util.Optional;
 
 import org.cryptomator.crypto.engine.Cryptor;
@@ -52,11 +50,7 @@ abstract class CryptoNode implements Node {
 
 	@Override
 	public boolean exists() {
-		try {
-			return parent.children().anyMatch(node -> node.equals(this));
-		} catch (IOException e) {
-			throw new UncheckedIOException(e);
-		}
+		return parent.children().anyMatch(node -> node.equals(this));
 	}
 
 	@Override

+ 61 - 12
main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java

@@ -28,35 +28,84 @@ public class CryptoFileSystemTest {
 	private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemTest.class);
 
 	@Test
-	public void testFilenameEncryption() throws UncheckedIOException, IOException {
+	public void testVaultStructureInitialization() throws UncheckedIOException, IOException {
 		// mock cryptor:
-		Cryptor cryptor = new NoCryptor();
+		final Cryptor cryptor = new NoCryptor();
 
 		// some mock fs:
-		FileSystem physicalFs = new InMemoryFileSystem();
-		Folder physicalDataRoot = physicalFs.folder("d");
+		final FileSystem physicalFs = new InMemoryFileSystem();
+		final Folder physicalDataRoot = physicalFs.folder("d");
 		Assert.assertFalse(physicalDataRoot.exists());
 
 		// init crypto fs:
-		FileSystem fs = new CryptoFileSystem(physicalFs, cryptor);
+		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor);
 		fs.create(FolderCreateMode.INCLUDING_PARENTS);
 		Assert.assertTrue(physicalDataRoot.exists());
 		Assert.assertEquals(physicalFs.children().count(), 2);
 		Assert.assertEquals(1, physicalDataRoot.files().count()); // ROOT file
 		Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory
 
+		LOG.debug(DirectoryPrinter.print(physicalFs));
+	}
+
+	@Test
+	public void testDirectoryCreation() throws UncheckedIOException, IOException {
+		// mock stuff and prepare crypto FS:
+		final Cryptor cryptor = new NoCryptor();
+		final FileSystem physicalFs = new InMemoryFileSystem();
+		final Folder physicalDataRoot = physicalFs.folder("d");
+		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor);
+		fs.create(FolderCreateMode.INCLUDING_PARENTS);
+
 		// add another encrypted folder:
-		Folder fooFolder = fs.folder("foo");
-		Folder barFolder = fooFolder.folder("bar");
+		final Folder fooFolder = fs.folder("foo");
+		final Folder fooBarFolder = fooFolder.folder("bar");
 		Assert.assertFalse(fooFolder.exists());
-		Assert.assertFalse(barFolder.exists());
-		barFolder.create(FolderCreateMode.INCLUDING_PARENTS);
+		Assert.assertFalse(fooBarFolder.exists());
+		fooBarFolder.create(FolderCreateMode.INCLUDING_PARENTS);
 		Assert.assertTrue(fooFolder.exists());
-		Assert.assertTrue(barFolder.exists());
+		Assert.assertTrue(fooBarFolder.exists());
 		Assert.assertEquals(3, countDataFolders(physicalDataRoot)); // parent + foo + bar
 
-		LOG.info(DirectoryPrinter.print(fs));
-		LOG.info(DirectoryPrinter.print(physicalFs));
+		LOG.debug(DirectoryPrinter.print(fs));
+	}
+
+	@Test
+	public void testDirectoryMoving() throws UncheckedIOException, IOException {
+		// mock stuff and prepare crypto FS:
+		final Cryptor cryptor = new NoCryptor();
+		final FileSystem physicalFs = new InMemoryFileSystem();
+		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor);
+		fs.create(FolderCreateMode.INCLUDING_PARENTS);
+
+		// create foo/bar/ and then move foo/ to baz/:
+		final Folder fooFolder = fs.folder("foo");
+		final Folder fooBarFolder = fooFolder.folder("bar");
+		final Folder bazFolder = fs.folder("baz");
+		final Folder bazBarFolder = bazFolder.folder("bar");
+		fooBarFolder.create(FolderCreateMode.INCLUDING_PARENTS);
+		Assert.assertTrue(fooBarFolder.exists());
+		Assert.assertFalse(bazFolder.exists());
+		fooFolder.moveTo(bazFolder);
+		// foo/bar/ should no longer exist, but baz/bar/ should:
+		Assert.assertFalse(fooBarFolder.exists());
+		Assert.assertTrue(bazFolder.exists());
+		Assert.assertTrue(bazBarFolder.exists());
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testDirectoryMovingWithinBloodline() throws UncheckedIOException, IOException {
+		// mock stuff and prepare crypto FS:
+		final Cryptor cryptor = new NoCryptor();
+		final FileSystem physicalFs = new InMemoryFileSystem();
+		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor);
+		fs.create(FolderCreateMode.INCLUDING_PARENTS);
+
+		// create foo/bar/ and then try to move foo/bar/ to foo/
+		final Folder fooFolder = fs.folder("foo");
+		final Folder fooBarFolder = fooFolder.folder("bar");
+		fooBarFolder.create(FolderCreateMode.INCLUDING_PARENTS);
+		fooBarFolder.moveTo(fooFolder);
 	}
 
 	/**

+ 9 - 14
main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/DirectoryWalker.java

@@ -8,8 +8,6 @@
  *******************************************************************************/
 package org.cryptomator.crypto.fs;
 
-import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.util.function.Consumer;
 
 import org.cryptomator.filesystem.Folder;
@@ -25,19 +23,16 @@ final class DirectoryWalker {
 	}
 
 	public static void walk(Folder folder, int depth, int maxDepth, Consumer<Node> visitor) {
-		try {
-			folder.files().forEach(visitor);
-			if (depth == maxDepth) {
-				return;
-			} else {
-				folder.folders().forEach(childFolder -> {
-					visitor.accept(childFolder);
-					walk(childFolder, depth + 1, maxDepth, visitor);
-				});
-			}
-		} catch (IOException e) {
-			throw new UncheckedIOException(e);
+		folder.files().forEach(visitor);
+		if (depth == maxDepth) {
+			return;
+		} else {
+			folder.folders().forEach(childFolder -> {
+				visitor.accept(childFolder);
+				walk(childFolder, depth + 1, maxDepth, visitor);
+			});
 		}
+
 	}
 
 }

+ 32 - 5
main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java

@@ -5,8 +5,11 @@
  ******************************************************************************/
 package org.cryptomator.filesystem;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Stream;
 
 /**
@@ -55,14 +58,38 @@ public interface Folder extends Node {
 	Folder folder(String name) throws UncheckedIOException;
 
 	/**
-	 * Copies this directory and its contents to the given destination. If the
-	 * target exists it is deleted before performing the copy.
+	 * Creates the directory, if it doesn't exist yet. After successful invocation {@link #exists()} will return <code>true</code>
+	 * 
+	 * @param mode Depending on this option either the attempt is made to recursively create all parent directories or an exception is thrown if the parent doesn't exist yet.
+	 * @throws UncheckedIOException wrapping an {@link FileNotFoundException}, if mode is {@link FolderCreateMode#FAIL_IF_PARENT_IS_MISSING FAIL_IF_PARENT_IS_MISSING} and parent doesn't exist.
+	 */
+	void create(FolderCreateMode mode) throws UncheckedIOException;
+
+	/**
+	 * Copies this directory and its contents to the given destination.
+	 */
+	default void copyTo(Folder target) throws UncheckedIOException {
+		final Folder copy = target.folder(this.name());
+		copy.create(FolderCreateMode.INCLUDING_PARENTS);
+		folders().forEach(folder -> folder.copyTo(copy));
+		files().forEach(srcFile -> {
+			final File dstFile = copy.file(srcFile.name());
+			try (ReadableFile src = srcFile.openReadable(1, TimeUnit.SECONDS); WritableFile dst = dstFile.openWritable(1, TimeUnit.SECONDS)) {
+				src.copyTo(dst);
+			} catch (TimeoutException e) {
+				throw new UncheckedIOException(new IOException("Failed to lock file in time.", e));
+			}
+		});
+	}
+
+	/**
+	 * Deletes the directory including all child elements. Afterwards {@link #exists()} will return <code>false</code>.
 	 */
-	void copyTo(Folder target);
+	void delete() throws UncheckedIOException;
 
 	/**
-	 * Moves this directory and its contents to the given destination. If the
-	 * target exists it is deleted before performing the move.
+	 * Moves this directory and its contents to the given destination. If the target exists it is deleted before performing the move.
+	 * Afterwards {@link #exists()} will return <code>false</code> for this folder and any child nodes.
 	 */
 	void moveTo(Folder target);
 

+ 32 - 38
main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java

@@ -9,7 +9,6 @@
 package org.cryptomator.filesystem.inmem;
 
 import java.io.FileNotFoundException;
-import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.time.Instant;
@@ -30,9 +29,9 @@ class InMemoryFile extends InMemoryNode implements ReadableFile, WritableFile {
 	}
 
 	@Override
-	public ReadableFile openReadable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
+	public ReadableFile openReadable(long timeout, TimeUnit unit) throws TimeoutException {
 		if (!exists()) {
-			throw new FileNotFoundException(this.name() + " does not exist");
+			throw new UncheckedIOException(new FileNotFoundException(this.name() + " does not exist"));
 		}
 		try {
 			if (!lock.readLock().tryLock(timeout, unit)) {
@@ -45,7 +44,7 @@ class InMemoryFile extends InMemoryNode implements ReadableFile, WritableFile {
 	}
 
 	@Override
-	public WritableFile openWritable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
+	public WritableFile openWritable(long timeout, TimeUnit unit) throws TimeoutException {
 		try {
 			if (!lock.writeLock().tryLock(timeout, unit)) {
 				throw new TimeoutException("Failed to open " + name() + " for writing within time limit.");
@@ -54,38 +53,34 @@ class InMemoryFile extends InMemoryNode implements ReadableFile, WritableFile {
 			Thread.currentThread().interrupt();
 		}
 		final InMemoryFolder parent = parent().get();
-		try {
-			parent.children.compute(this.name(), (k, v) -> {
-				if (v != null && v != this) {
-					throw new IllegalStateException("More than one representation of same file");
-				}
-				return this;
-			});
-		} catch (UncheckedIOException e) {
-			throw e.getCause();
-		}
+		parent.children.compute(this.name(), (k, v) -> {
+			if (v != null && v != this) {
+				throw new IllegalStateException("More than one representation of same file");
+			}
+			return this;
+		});
 		return this;
 	}
 
 	@Override
-	public void read(ByteBuffer target) throws IOException {
+	public void read(ByteBuffer target) {
 		this.read(target, 0);
 	}
 
 	@Override
-	public void read(ByteBuffer target, int position) throws IOException {
+	public void read(ByteBuffer target, int position) {
 		content.rewind();
 		content.position(position);
 		target.put(content);
 	}
 
 	@Override
-	public void write(ByteBuffer source) throws IOException {
+	public void write(ByteBuffer source) {
 		this.write(source, content.position());
 	}
 
 	@Override
-	public void write(ByteBuffer source, int position) throws IOException {
+	public void write(ByteBuffer source, int position) {
 		assert content != null;
 		if (position + source.remaining() > content.remaining()) {
 			// create bigger buffer
@@ -97,43 +92,42 @@ class InMemoryFile extends InMemoryNode implements ReadableFile, WritableFile {
 		content.put(source);
 	}
 
-	@Override
-	public WritableFile moveTo(WritableFile other) throws IOException {
-		this.copyTo(other);
-		this.delete();
-		return other;
-	}
-
 	@Override
 	public void setLastModified(Instant instant) {
 		this.lastModified = instant;
 	}
 
-	@Override
-	public void delete() {
-		final InMemoryFolder parent = parent().get();
-		parent.children.computeIfPresent(this.name(), (k, v) -> {
-			truncate();
-			// returning null removes the entry.
-			return null;
-		});
-	}
-
 	@Override
 	public void truncate() {
 		content = ByteBuffer.wrap(new byte[0]);
 	}
 
 	@Override
-	public WritableFile copyTo(WritableFile other) throws IOException {
+	public void copyTo(WritableFile other) {
 		content.rewind();
 		other.truncate();
 		other.write(content);
-		return other;
 	}
 
 	@Override
-	public void close() throws IOException {
+	public void moveTo(WritableFile other) {
+		this.copyTo(other);
+		this.delete();
+	}
+
+	@Override
+	public void delete() {
+		final InMemoryFolder parent = parent().get();
+		parent.children.computeIfPresent(this.name(), (k, v) -> {
+			truncate();
+			// returning null removes the entry.
+			return null;
+		});
+		assert!this.exists();
+	}
+
+	@Override
+	public void close() {
 		if (lock.isWriteLockedByCurrentThread()) {
 			lock.writeLock().unlock();
 		} else if (lock.getReadHoldCount() > 0) {

+ 28 - 14
main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java

@@ -9,7 +9,6 @@
 package org.cryptomator.filesystem.inmem;
 
 import java.io.FileNotFoundException;
-import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.file.FileAlreadyExistsException;
 import java.time.Instant;
@@ -67,36 +66,51 @@ class InMemoryFolder extends InMemoryNode implements Folder {
 	}
 
 	@Override
-	public void create(FolderCreateMode mode) throws IOException {
+	public void create(FolderCreateMode mode) {
 		if (exists()) {
 			return;
 		}
 		if (!parent.exists() && FolderCreateMode.FAIL_IF_PARENT_IS_MISSING.equals(mode)) {
-			throw new FileNotFoundException(parent.name);
+			throw new UncheckedIOException(new FileNotFoundException(parent.name));
 		} else if (!parent.exists() && FolderCreateMode.INCLUDING_PARENTS.equals(mode)) {
 			parent.create(mode);
 		}
 		assert parent.exists();
-		try {
-			parent.children.compute(this.name(), (k, v) -> {
-				if (v == null) {
-					this.lastModified = Instant.now();
-					return this;
-				} else {
-					throw new UncheckedIOException(new FileExistsException(k));
-				}
-			});
-		} catch (UncheckedIOException e) {
-			throw e.getCause();
+		parent.children.compute(this.name(), (k, v) -> {
+			if (v == null) {
+				this.lastModified = Instant.now();
+				return this;
+			} else {
+				throw new UncheckedIOException(new FileExistsException(k));
+			}
+		});
+		assert this.exists();
+	}
+
+	@Override
+	public void moveTo(Folder target) {
+		if (target.exists()) {
+			target.delete();
 		}
+		assert!target.exists();
+		target.create(FolderCreateMode.INCLUDING_PARENTS);
+		this.copyTo(target);
+		this.delete();
+		assert!this.exists();
 	}
 
 	@Override
 	public void delete() {
+		// delete subfolder recursively:
+		folders().forEach(Folder::delete);
+		// delete direct children (this deletes files):
+		this.children.clear();
+		// remove ourself from parent:
 		parent.children.computeIfPresent(name, (k, v) -> {
 			// returning null removes the entry.
 			return null;
 		});
+		assert!this.exists();
 	}
 
 	@Override

+ 31 - 3
main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java

@@ -8,7 +8,6 @@
  *******************************************************************************/
 package org.cryptomator.filesystem.inmem;
 
-import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -25,7 +24,7 @@ import org.junit.Test;
 public class InMemoryFileSystemTest {
 
 	@Test
-	public void testFolderCreation() throws IOException {
+	public void testFolderCreation() {
 		final FileSystem fs = new InMemoryFileSystem();
 		Folder fooFolder = fs.folder("foo");
 
@@ -54,7 +53,7 @@ public class InMemoryFileSystemTest {
 	}
 
 	@Test
-	public void testFileReadCopyMoveWrite() throws IOException, TimeoutException {
+	public void testFileReadCopyMoveWrite() throws TimeoutException {
 		final FileSystem fs = new InMemoryFileSystem();
 		File fooFile = fs.file("foo.txt");
 
@@ -96,7 +95,36 @@ public class InMemoryFileSystemTest {
 			readable.read(readBuf, 6);
 		}
 		Assert.assertEquals("world", new String(readBuf.array()));
+	}
+
+	@Test
+	public void testFolderCopy() throws TimeoutException {
+		final FileSystem fs = new InMemoryFileSystem();
+		final Folder fooBarFolder = fs.folder("foo").folder("bar");
+		final Folder qweAsdFolder = fs.folder("qwe").folder("asd");
+		final Folder qweAsdBarFolder = qweAsdFolder.folder("bar");
+		final File test1File = fooBarFolder.file("test1.txt");
+		final File test2File = fooBarFolder.file("test2.txt");
+		fooBarFolder.create(FolderCreateMode.INCLUDING_PARENTS);
 
+		// create some files inside foo/bar/
+		try (WritableFile writable1 = test1File.openWritable(1, TimeUnit.SECONDS); //
+				WritableFile writable2 = test2File.openWritable(1, TimeUnit.SECONDS)) {
+			writable1.write(ByteBuffer.wrap("hello".getBytes()));
+			writable2.write(ByteBuffer.wrap("world".getBytes()));
+		}
+		Assert.assertTrue(test1File.exists());
+		Assert.assertTrue(test2File.exists());
+
+		// copy foo/bar/ to qwe/asd/ (result is qwe/asd/bar/file1.txt & qwe/asd/bar/file2.txt)
+		fooBarFolder.copyTo(qweAsdFolder);
+		Assert.assertTrue(qweAsdBarFolder.exists());
+		Assert.assertEquals(1, qweAsdFolder.folders().count());
+		Assert.assertEquals(2, qweAsdBarFolder.files().count());
+
+		// make sure original files still exist:
+		Assert.assertTrue(test1File.exists());
+		Assert.assertTrue(test2File.exists());
 	}
 
 }