mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-08-17 20:56:02 +00:00
first tests with refactored io layers
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target/
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.11.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>crypto-layer</artifactId>
|
||||
<name>Crypto Layer</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<CryptoFolder> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> 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<CryptoFile> 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<CryptoFolder> 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<CryptoFolder> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
/target/
|
||||
/target/
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.Optional;
|
||||
public interface FileSystem extends Folder {
|
||||
|
||||
@Override
|
||||
default Optional<Folder> parent() {
|
||||
default Optional<? extends Folder> parent() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,11 @@ public interface Folder extends Node {
|
||||
* if an {@link IOException} occurs while initializing the
|
||||
* stream
|
||||
*/
|
||||
Stream<Node> 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<File> 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<Folder> folders() throws IOException {
|
||||
default Stream<? extends Folder> folders() throws IOException {
|
||||
return children() //
|
||||
.filter(Folder.class::isInstance) //
|
||||
.map(Folder.class::cast);
|
||||
|
||||
@@ -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<Folder> 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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -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<InMemoryFolder> parent() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
// no-op.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "/";
|
||||
}
|
||||
|
||||
}
|
||||
+99
@@ -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<String, InMemoryNode> children = new TreeMap<>();
|
||||
final Map<String, InMemoryNode> volatileChildren = new HashMap<>();
|
||||
|
||||
InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified) {
|
||||
super(parent, name, lastModified);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<InMemoryNode> 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 + "/";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<InMemoryFolder> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+94
@@ -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()));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -202,6 +202,7 @@
|
||||
<module>crypto-aes</module>
|
||||
<module>core</module>
|
||||
<module>ui</module>
|
||||
<module>crypto-layer</module>
|
||||
</modules>
|
||||
|
||||
<profiles>
|
||||
|
||||
Reference in New Issue
Block a user