diff --git a/main/crypto-layer/.gitignore b/main/crypto-layer/.gitignore
new file mode 100644
index 000000000..b83d22266
--- /dev/null
+++ b/main/crypto-layer/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/main/crypto-layer/pom.xml b/main/crypto-layer/pom.xml
new file mode 100644
index 000000000..4f74e5233
--- /dev/null
+++ b/main/crypto-layer/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ 4.0.0
+
+ org.cryptomator
+ main
+ 0.11.0-SNAPSHOT
+
+ crypto-layer
+ Crypto Layer
+
+
+
+ org.cryptomator
+ filesystem-api
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ commons-codec
+ commons-codec
+
+
+
\ No newline at end of file
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFile.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFile.java
new file mode 100644
index 000000000..1a64d402c
--- /dev/null
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFile.java
@@ -0,0 +1,44 @@
+package org.cryptomator.crypto;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+
+public class CryptoFile extends CryptoNode implements File {
+
+ private static final String ENCRYPTED_FILE_EXT = ".file";
+
+ public CryptoFile(CryptoFolder parent, String name) {
+ super(parent, name);
+ }
+
+ @Override
+ String encryptedName() {
+ return name() + ENCRYPTED_FILE_EXT;
+ }
+
+ @Override
+ public Instant lastModified() throws UncheckedIOException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public ReadableFile openReadable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public WritableFile openWritable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+}
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFileSystem.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFileSystem.java
new file mode 100644
index 000000000..e03c72d48
--- /dev/null
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFileSystem.java
@@ -0,0 +1,74 @@
+package org.cryptomator.crypto;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.FolderCreateMode;
+
+public class CryptoFileSystem extends CryptoFolder implements FileSystem {
+
+ private static final String DATA_ROOT_DIR = "d";
+ private static final String METADATA_ROOT_DIR = "m";
+ private static final String ROOT_DIR_FILE = "root";
+ private static final String MASTERKEY_FILE = "masterkey.cryptomator";
+ private static final String MASTERKEY_BACKUP_FILE = "masterkey.cryptomator.bkup";
+
+ private final Folder physicalRoot;
+
+ public CryptoFileSystem(Folder physicalRoot) {
+ super(null, "");
+ this.physicalRoot = physicalRoot;
+ }
+
+ @Override
+ File physicalFile() throws IOException {
+ return physicalDataRoot().file(ROOT_DIR_FILE);
+ }
+
+ @Override
+ Folder physicalFolder() throws IOException {
+ // TODO Auto-generated method stub
+ return super.physicalFolder();
+ }
+
+ @Override
+ Folder physicalDataRoot() {
+ return physicalRoot.folder(DATA_ROOT_DIR);
+ }
+
+ @Override
+ Folder physicalMetadataRoot() {
+ return physicalRoot.folder(METADATA_ROOT_DIR);
+ }
+
+ @Override
+ public Optional parent() {
+ return Optional.empty();
+ }
+
+ @Override
+ public boolean exists() {
+ return physicalRoot.exists();
+ }
+
+ @Override
+ public void delete() {
+ // no-op.
+ }
+
+ @Override
+ public String toString() {
+ return "/";
+ }
+
+ @Override
+ public void create(FolderCreateMode mode) throws IOException {
+ physicalDataRoot().create(mode);
+ physicalMetadataRoot().create(mode);
+ super.create(mode);
+ }
+
+}
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFolder.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFolder.java
new file mode 100644
index 000000000..d45109568
--- /dev/null
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFolder.java
@@ -0,0 +1,146 @@
+package org.cryptomator.crypto;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.Instant;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+import org.apache.commons.codec.binary.Base32;
+import org.apache.commons.lang3.StringUtils;
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.FolderCreateMode;
+import org.cryptomator.filesystem.Node;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+
+class CryptoFolder extends CryptoNode implements Folder {
+
+ private static final String ENCRYPTED_FILE_EXT = ".file";
+ private static final String ENCRYPTED_DIR_EXT = ".dir";
+
+ private final AtomicReference directoryId = new AtomicReference<>();
+
+ public CryptoFolder(CryptoFolder parent, String name) {
+ super(parent, name);
+ }
+
+ @Override
+ String encryptedName() {
+ return name() + ENCRYPTED_DIR_EXT;
+ }
+
+ protected String getDirectoryId() throws IOException {
+ if (directoryId.get() == null) {
+ File dirFile = physicalFile();
+ if (dirFile.exists()) {
+ try (ReadableFile readable = dirFile.openReadable(1, TimeUnit.SECONDS)) {
+ final ByteBuffer buf = ByteBuffer.allocate(64);
+ readable.read(buf);
+ buf.flip();
+ byte[] bytes = new byte[buf.remaining()];
+ 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);
+ }
+ } else {
+ directoryId.compareAndSet(null, UUID.randomUUID().toString());
+ }
+ }
+ return directoryId.get();
+ }
+
+ File physicalFile() throws IOException {
+ return parent.physicalFolder().file(encryptedName());
+ }
+
+ Folder physicalFolder() throws IOException {
+ final String encryptedThenHashedDirId;
+ try {
+ final byte[] hash = MessageDigest.getInstance("SHA-1").digest(getDirectoryId().getBytes());
+ encryptedThenHashedDirId = new Base32().encodeAsString(hash);
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError("SHA-1 exists in every JVM");
+ }
+ // TODO actual encryption
+ return physicalDataRoot().folder(encryptedThenHashedDirId.substring(0, 2)).folder(encryptedThenHashedDirId.substring(2));
+ }
+
+ @Override
+ public Instant lastModified() {
+ try {
+ return physicalFile().lastModified();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public Stream extends Node> children() throws IOException {
+ return Stream.concat(files(), folders());
+ }
+
+ @Override
+ public Stream files() throws IOException {
+ return physicalFolder().files().map(File::name).filter(s -> s.endsWith(ENCRYPTED_FILE_EXT)).map(this::decryptFileName).map(this::file);
+ }
+
+ private String decryptFileName(String encryptedFileName) {
+ // TODO Auto-generated method stub
+ return StringUtils.removeEnd(encryptedFileName, ENCRYPTED_FILE_EXT);
+ }
+
+ @Override
+ public CryptoFile file(String name) {
+ return new CryptoFile(this, name);
+ }
+
+ @Override
+ public Stream folders() throws IOException {
+ return physicalFolder().files().map(File::name).filter(s -> s.endsWith(ENCRYPTED_DIR_EXT)).map(this::decryptFolderName).map(this::folder);
+ }
+
+ private String decryptFolderName(String encryptedFolderName) {
+ // TODO Auto-generated method stub
+ return StringUtils.removeEnd(encryptedFolderName, ENCRYPTED_DIR_EXT);
+ }
+
+ @Override
+ public CryptoFolder folder(String name) {
+ return new CryptoFolder(this, name);
+ }
+
+ @Override
+ public void create(FolderCreateMode mode) throws IOException {
+ final File dirFile = physicalFile();
+ if (dirFile.exists()) {
+ return;
+ } else {
+ final String directoryId = getDirectoryId();
+ try (WritableFile writable = dirFile.openWritable(1, TimeUnit.SECONDS)) {
+ 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);
+ }
+ physicalFolder().create(FolderCreateMode.INCLUDING_PARENTS);
+ }
+ }
+
+ @Override
+ public void delete() throws IOException {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoNode.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoNode.java
new file mode 100644
index 000000000..dc38a2a16
--- /dev/null
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoNode.java
@@ -0,0 +1,72 @@
+package org.cryptomator.crypto;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Optional;
+
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.Node;
+
+abstract class CryptoNode implements Node {
+
+ protected final CryptoFolder parent;
+ protected final String name;
+
+ public CryptoNode(CryptoFolder parent, String name) {
+ this.parent = parent;
+ this.name = name;
+ }
+
+ Folder physicalDataRoot() {
+ return parent.physicalDataRoot();
+ }
+
+ Folder physicalMetadataRoot() {
+ return parent.physicalMetadataRoot();
+ }
+
+ @Override
+ public Optional parent() {
+ return Optional.of(parent);
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ String encryptedName() {
+ return name();
+ }
+
+ @Override
+ public boolean exists() {
+ try {
+ return parent.children().anyMatch(node -> node.equals(this));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((parent == null) ? 0 : parent.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof CryptoNode) {
+ CryptoNode other = (CryptoNode) obj;
+ return this.getClass() == other.getClass() //
+ && (this.parent == null && other.parent == null || this.parent.equals(other.parent)) //
+ && (this.name == null && other.name == null || this.name.equals(other.name));
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/package-info.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/package-info.java
new file mode 100644
index 000000000..765e4fefa
--- /dev/null
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Provides a decoration layer for the {@link org.cryptomator.filesystem} API, consuming an encrypted file system and providing access to a cleartext filesystem.
+ */
+package org.cryptomator.crypto;
\ No newline at end of file
diff --git a/main/crypto-layer/src/test/java/org/cryptomator/crypto/CryptoFileSystemTest.java b/main/crypto-layer/src/test/java/org/cryptomator/crypto/CryptoFileSystemTest.java
new file mode 100644
index 000000000..4f459df8f
--- /dev/null
+++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/CryptoFileSystemTest.java
@@ -0,0 +1,38 @@
+package org.cryptomator.crypto;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.FolderCreateMode;
+import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class CryptoFileSystemTest {
+
+ @Test
+ public void testFilenameEncryption() throws UncheckedIOException, IOException {
+ // some mock fs:
+ FileSystem physicalFs = new InMemoryFileSystem();
+ Folder dataRoot = physicalFs.folder("d");
+ Assert.assertFalse(dataRoot.exists());
+
+ // init crypto fs:
+ FileSystem fs = new CryptoFileSystem(physicalFs);
+ fs.create(FolderCreateMode.INCLUDING_PARENTS);
+ Assert.assertTrue(dataRoot.exists());
+ Assert.assertEquals(physicalFs.children().count(), 2);
+ Assert.assertEquals(1, dataRoot.files().count()); // ROOT file
+ Assert.assertEquals(1, dataRoot.folders().count()); // ROOT directory
+
+ // add another encrypted folder:
+ Folder testFolder = fs.folder("test");
+ Assert.assertFalse(testFolder.exists());
+ testFolder.create(FolderCreateMode.INCLUDING_PARENTS);
+ Assert.assertTrue(testFolder.exists());
+ Assert.assertEquals(2, dataRoot.folders().count());
+ }
+
+}
diff --git a/main/filesystem-api/.gitignore b/main/filesystem-api/.gitignore
index b83d22266..1dd333108 100644
--- a/main/filesystem-api/.gitignore
+++ b/main/filesystem-api/.gitignore
@@ -1 +1,2 @@
/target/
+/target/
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java
index 44b259ec1..5a61ac655 100644
--- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java
@@ -7,11 +7,12 @@ package org.cryptomator.filesystem;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
public interface File extends Node {
- ReadableFile openReadable(long timeout, TimeUnit unit) throws IOException;
+ ReadableFile openReadable(long timeout, TimeUnit unit) throws IOException, TimeoutException;
- WritableFile openWritable(long timeout, TimeUnit unit) throws IOException;
+ WritableFile openWritable(long timeout, TimeUnit unit) throws IOException, TimeoutException;
}
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystem.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystem.java
index 7dfb83e68..6a1343b39 100644
--- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystem.java
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystem.java
@@ -15,7 +15,7 @@ import java.util.Optional;
public interface FileSystem extends Folder {
@Override
- default Optional parent() {
+ default Optional extends Folder> parent() {
return Optional.empty();
}
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java
index e3bcb678d..18e7ffe23 100644
--- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java
@@ -32,11 +32,11 @@ public interface Folder extends Node {
* if an {@link IOException} occurs while initializing the
* stream
*/
- Stream children() throws IOException;
+ Stream extends Node> children() throws IOException;
- File file(String name) throws IOException;
+ File file(String name) throws UncheckedIOException;
- Folder folder(String name) throws IOException;
+ Folder folder(String name) throws UncheckedIOException;
void create(FolderCreateMode mode) throws IOException;
@@ -46,7 +46,7 @@ public interface Folder extends Node {
* @return the result of {@link #children()} filtered to contain only
* {@link File Files}
*/
- default Stream files() throws IOException {
+ default Stream extends File> files() throws IOException {
return children() //
.filter(File.class::isInstance) //
.map(File.class::cast);
@@ -56,7 +56,7 @@ public interface Folder extends Node {
* @return the result of {@link #children()} filtered to contain only
* {@link Folder Folders}
*/
- default Stream folders() throws IOException {
+ default Stream extends Folder> folders() throws IOException {
return children() //
.filter(Folder.class::isInstance) //
.map(Folder.class::cast);
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java
index b18566a05..f0fcb52cd 100644
--- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java
@@ -5,7 +5,7 @@
******************************************************************************/
package org.cryptomator.filesystem;
-import java.io.IOException;
+import java.io.UncheckedIOException;
import java.time.Instant;
import java.util.Optional;
@@ -19,12 +19,12 @@ import java.util.Optional;
*/
public interface Node {
- String name() throws IOException;
+ String name() throws UncheckedIOException;
- Optional parent() throws IOException;
+ Optional extends Folder> parent() throws UncheckedIOException;
- boolean exists() throws IOException;
+ boolean exists() throws UncheckedIOException;
- Instant lastModified() throws IOException;
+ Instant lastModified() throws UncheckedIOException;
}
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
new file mode 100644
index 000000000..a6cc92070
--- /dev/null
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
@@ -0,0 +1,141 @@
+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;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+
+public class InMemoryFile extends InMemoryNode implements ReadableFile, WritableFile {
+
+ private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+ private ByteBuffer content = ByteBuffer.wrap(new byte[0]);
+
+ InMemoryFile(InMemoryFolder parent, String name, Instant lastModified) {
+ super(parent, name, lastModified);
+ }
+
+ @Override
+ public ReadableFile openReadable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
+ if (!exists()) {
+ throw new FileNotFoundException(this.name() + " does not exist");
+ }
+ try {
+ if (!lock.readLock().tryLock(timeout, unit)) {
+ throw new TimeoutException("Failed to open " + name() + " for reading within time limit.");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return this;
+ }
+
+ @Override
+ public WritableFile openWritable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
+ try {
+ if (!lock.writeLock().tryLock(timeout, unit)) {
+ throw new TimeoutException("Failed to open " + name() + " for writing within time limit.");
+ }
+ } catch (InterruptedException e) {
+ 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();
+ }
+ return this;
+ }
+
+ @Override
+ public void read(ByteBuffer target) throws IOException {
+ this.read(target, 0);
+ }
+
+ @Override
+ public void read(ByteBuffer target, int position) throws IOException {
+ content.rewind();
+ content.position(position);
+ target.put(content);
+ }
+
+ @Override
+ public void write(ByteBuffer source) throws IOException {
+ this.write(source, content.position());
+ }
+
+ @Override
+ public void write(ByteBuffer source, int position) throws IOException {
+ assert content != null;
+ if (position + source.remaining() > content.remaining()) {
+ // create bigger buffer
+ ByteBuffer tmp = ByteBuffer.allocate(Math.max(position, content.capacity()) + source.remaining());
+ tmp.put(content);
+ content = tmp;
+ }
+ content.position(position);
+ 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 {
+ content.rewind();
+ other.truncate();
+ other.write(content);
+ return other;
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (lock.isWriteLockedByCurrentThread()) {
+ lock.writeLock().unlock();
+ } else if (lock.getReadHoldCount() > 0) {
+ lock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return parent.toString() + name;
+ }
+
+}
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java
new file mode 100644
index 000000000..86f403a86
--- /dev/null
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java
@@ -0,0 +1,34 @@
+package org.cryptomator.filesystem.inmem;
+
+import java.time.Instant;
+import java.util.Optional;
+
+import org.cryptomator.filesystem.FileSystem;
+
+public class InMemoryFileSystem extends InMemoryFolder implements FileSystem {
+
+ public InMemoryFileSystem() {
+ super(null, "", Instant.now());
+ }
+
+ @Override
+ public Optional parent() {
+ return Optional.empty();
+ }
+
+ @Override
+ public boolean exists() {
+ return true;
+ }
+
+ @Override
+ public void delete() {
+ // no-op.
+ }
+
+ @Override
+ public String toString() {
+ return "/";
+ }
+
+}
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java
new file mode 100644
index 000000000..6bb1cbbec
--- /dev/null
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java
@@ -0,0 +1,99 @@
+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;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Stream;
+
+import org.apache.commons.io.FileExistsException;
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.FolderCreateMode;
+
+public class InMemoryFolder extends InMemoryNode implements Folder {
+
+ final Map children = new TreeMap<>();
+ final Map volatileChildren = new HashMap<>();
+
+ InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified) {
+ super(parent, name, lastModified);
+ }
+
+ @Override
+ public Stream children() {
+ return children.values().stream();
+ }
+
+ @Override
+ public InMemoryFile file(String name) {
+ InMemoryNode node = children.get(name);
+ if (node == null) {
+ node = volatileChildren.computeIfAbsent(name, (k) -> {
+ return new InMemoryFile(this, name, Instant.MIN);
+ });
+ }
+ if (node instanceof InMemoryFile) {
+ return (InMemoryFile) node;
+ } else {
+ throw new UncheckedIOException(new FileAlreadyExistsException(name + " exists, but is not a file."));
+ }
+ }
+
+ @Override
+ public InMemoryFolder folder(String name) {
+ InMemoryNode node = children.get(name);
+ if (node == null) {
+ node = volatileChildren.computeIfAbsent(name, (k) -> {
+ return new InMemoryFolder(this, name, Instant.MIN);
+ });
+ }
+ if (node instanceof InMemoryFolder) {
+ return (InMemoryFolder) node;
+ } else {
+ throw new UncheckedIOException(new FileAlreadyExistsException(name + " exists, but is not a folder."));
+ }
+ }
+
+ @Override
+ public void create(FolderCreateMode mode) throws IOException {
+ if (exists()) {
+ return;
+ }
+ if (!parent.exists() && FolderCreateMode.FAIL_IF_PARENT_IS_MISSING.equals(mode)) {
+ throw 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();
+ }
+ }
+
+ @Override
+ public void delete() {
+ parent.children.computeIfPresent(name, (k, v) -> {
+ // returning null removes the entry.
+ return null;
+ });
+ }
+
+ @Override
+ public String toString() {
+ return parent.toString() + name + "/";
+ }
+
+}
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java
new file mode 100644
index 000000000..53dd7d833
--- /dev/null
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java
@@ -0,0 +1,61 @@
+package org.cryptomator.filesystem.inmem;
+
+import java.time.Instant;
+import java.util.Optional;
+
+import org.cryptomator.filesystem.Node;
+
+public class InMemoryNode implements Node {
+
+ protected final InMemoryFolder parent;
+ protected final String name;
+ protected Instant lastModified;
+
+ InMemoryNode(InMemoryFolder parent, String name, Instant lastModified) {
+ this.parent = parent;
+ this.name = name;
+ this.lastModified = lastModified;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public Optional parent() {
+ return Optional.of(parent);
+ }
+
+ @Override
+ public boolean exists() {
+ return parent.children().anyMatch(node -> node.equals(this));
+ }
+
+ @Override
+ public Instant lastModified() {
+ return lastModified;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((parent == null) ? 0 : parent.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof InMemoryNode) {
+ InMemoryNode other = (InMemoryNode) obj;
+ return this.getClass() == other.getClass() //
+ && (this.parent == null && other.parent == null || this.parent.equals(other.parent)) //
+ && (this.name == null && other.name == null || this.name.equals(other.name));
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/main/filesystem-api/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java b/main/filesystem-api/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java
new file mode 100644
index 000000000..3b04ac297
--- /dev/null
+++ b/main/filesystem-api/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java
@@ -0,0 +1,94 @@
+package org.cryptomator.filesystem.inmem;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.FolderCreateMode;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class InMemoryFileSystemTest {
+
+ @Test
+ public void testFolderCreation() throws IOException {
+ final FileSystem fs = new InMemoryFileSystem();
+ Folder fooFolder = fs.folder("foo");
+
+ // nothing happened yet:
+ Assert.assertFalse(fooFolder.exists());
+ Assert.assertEquals(0, fs.folders().count());
+
+ // create /foo
+ fooFolder.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
+ Assert.assertTrue(fooFolder.exists());
+ Assert.assertEquals(1, fs.folders().count());
+
+ // delete /foo
+ fooFolder.delete();
+ Assert.assertFalse(fooFolder.exists());
+ Assert.assertEquals(0, fs.folders().count());
+
+ // create /foo/bar
+ Folder fooBarFolder = fooFolder.folder("bar");
+ Assert.assertFalse(fooBarFolder.exists());
+ fooBarFolder.create(FolderCreateMode.INCLUDING_PARENTS);
+ Assert.assertTrue(fooFolder.exists());
+ Assert.assertTrue(fooBarFolder.exists());
+ Assert.assertEquals(1, fs.folders().count());
+ Assert.assertEquals(1, fooFolder.folders().count());
+ }
+
+ @Test
+ public void testFileReadCopyMoveWrite() throws IOException, TimeoutException {
+ final FileSystem fs = new InMemoryFileSystem();
+ File fooFile = fs.file("foo.txt");
+
+ // nothing happened yet:
+ Assert.assertFalse(fooFile.exists());
+ Assert.assertEquals(0, fs.files().count());
+
+ // write "hello world" to foo
+ try (WritableFile writable = fooFile.openWritable(1, TimeUnit.SECONDS)) {
+ writable.write(ByteBuffer.wrap("hello".getBytes()));
+ writable.write(ByteBuffer.wrap(" ".getBytes()));
+ writable.write(ByteBuffer.wrap("world".getBytes()));
+ }
+ Assert.assertTrue(fooFile.exists());
+
+ // copy foo to bar
+ File barFile = fs.file("bar.txt");
+ try (WritableFile writable = barFile.openWritable(1, TimeUnit.SECONDS)) {
+ try (ReadableFile readable = fooFile.openReadable(1, TimeUnit.SECONDS)) {
+ readable.copyTo(writable);
+ }
+ }
+ Assert.assertTrue(fooFile.exists());
+ Assert.assertTrue(barFile.exists());
+
+ // move bar to baz
+ File bazFile = fs.file("baz.txt");
+ try (WritableFile src = barFile.openWritable(1, TimeUnit.SECONDS)) {
+ try (WritableFile dst = bazFile.openWritable(1, TimeUnit.SECONDS)) {
+ src.moveTo(dst);
+ }
+ }
+ Assert.assertFalse(barFile.exists());
+ Assert.assertTrue(bazFile.exists());
+
+ // read "hello world" from baz
+ final ByteBuffer readBuf = ByteBuffer.allocate(5);
+ try (ReadableFile readable = bazFile.openReadable(1, TimeUnit.SECONDS)) {
+ readable.read(readBuf, 6);
+ }
+ Assert.assertEquals("world", new String(readBuf.array()));
+
+ }
+
+}
diff --git a/main/pom.xml b/main/pom.xml
index 1199de99f..54732d1a9 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -202,6 +202,7 @@
crypto-aes
core
ui
+ crypto-layer