From e1b74ce312930014a07961e173685109733fe1cb Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 14 Dec 2015 04:37:29 +0100 Subject: [PATCH] first tests with refactored io layers --- main/crypto-layer/.gitignore | 1 + main/crypto-layer/pom.xml | 35 +++++ .../org/cryptomator/crypto/CryptoFile.java | 44 ++++++ .../cryptomator/crypto/CryptoFileSystem.java | 74 +++++++++ .../org/cryptomator/crypto/CryptoFolder.java | 146 ++++++++++++++++++ .../org/cryptomator/crypto/CryptoNode.java | 72 +++++++++ .../org/cryptomator/crypto/package-info.java | 4 + .../crypto/CryptoFileSystemTest.java | 38 +++++ main/filesystem-api/.gitignore | 1 + .../java/org/cryptomator/filesystem/File.java | 5 +- .../cryptomator/filesystem/FileSystem.java | 2 +- .../org/cryptomator/filesystem/Folder.java | 10 +- .../java/org/cryptomator/filesystem/Node.java | 10 +- .../filesystem/inmem/InMemoryFile.java | 141 +++++++++++++++++ .../filesystem/inmem/InMemoryFileSystem.java | 34 ++++ .../filesystem/inmem/InMemoryFolder.java | 99 ++++++++++++ .../filesystem/inmem/InMemoryNode.java | 61 ++++++++ .../inmem/InMemoryFileSystemTest.java | 94 +++++++++++ main/pom.xml | 1 + 19 files changed, 859 insertions(+), 13 deletions(-) create mode 100644 main/crypto-layer/.gitignore create mode 100644 main/crypto-layer/pom.xml create mode 100644 main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFile.java create mode 100644 main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFileSystem.java create mode 100644 main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFolder.java create mode 100644 main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoNode.java create mode 100644 main/crypto-layer/src/main/java/org/cryptomator/crypto/package-info.java create mode 100644 main/crypto-layer/src/test/java/org/cryptomator/crypto/CryptoFileSystemTest.java create mode 100644 main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java create mode 100644 main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java create mode 100644 main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java create mode 100644 main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java create mode 100644 main/filesystem-api/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java 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 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 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 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 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 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 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