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 662fc6873..89c40ff6e 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
@@ -44,6 +44,16 @@ public interface Folder extends Node {
*/
File file(String name) throws UncheckedIOException;
+ /**
+ * Returns a file by resolving a path relative to this folder.
+ *
+ * @param path A unix-style path, which is always relative to this folder, no matter if it starts with a slash or not
+ * @return File with the given path relative to this folder
+ */
+ default File resolveFile(String relativePath) throws UncheckedIOException {
+ return PathResolver.resolveFile(this, relativePath);
+ }
+
/**
*
* Returns the child {@link Node} in this directory of type {@link Folder}
@@ -54,6 +64,16 @@ public interface Folder extends Node {
*/
Folder folder(String name) throws UncheckedIOException;
+ /**
+ * Returns a folder by resolving a path relative to this folder.
+ *
+ * @param path A unix-style path, which is always relative to this folder, no matter if it starts with a slash or not
+ * @return Folder with the given path relative to this folder
+ */
+ default Folder resolveFolder(String relativePath) throws UncheckedIOException {
+ return PathResolver.resolveFolder(this, relativePath);
+ }
+
/**
* Creates the directory including all parent directories, if it doesn't
* exist yet. No effect, if folder already exists.
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/PathResolver.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/PathResolver.java
new file mode 100644
index 000000000..6c456b2df
--- /dev/null
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/PathResolver.java
@@ -0,0 +1,100 @@
+package org.cryptomator.filesystem;
+
+import java.io.FileNotFoundException;
+import java.io.UncheckedIOException;
+import java.util.Arrays;
+import java.util.Iterator;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+
+public final class PathResolver {
+
+ private static final String DOT = ".";
+ private static final String DOTDOT = "..";
+
+ private PathResolver() {
+ }
+
+ /**
+ * Resolves a relative path (separated by '/') to a folder, e.g.
+ *
+ *
+ *
+ *
+ * | dir |
+ * path |
+ * result |
+ *
+ *
+ *
+ *
+ * | /foo/bar |
+ * foo/bar |
+ * /foo/bar/foo/bar |
+ *
+ *
+ * | /foo/bar |
+ * ../baz |
+ * /foo/baz |
+ *
+ *
+ * | /foo/bar |
+ * ./foo/.. |
+ * /foo/bar |
+ *
+ *
+ * | /foo/bar |
+ * ../../.. |
+ * Exception |
+ *
+ *
+ *
+ *
+ * @param dir The directory from which to resolve the path.
+ * @param relativePath The path relative to a given directory.
+ * @return The folder with the given path relative to the given dir.
+ */
+ public static Folder resolveFolder(Folder dir, String relativePath) {
+ final String[] fragments = StringUtils.split(relativePath, '/');
+ if (ArrayUtils.isEmpty(fragments)) {
+ throw new IllegalArgumentException("Empty relativePath");
+ }
+ return resolveFolder(dir, Arrays.stream(fragments).iterator());
+ }
+
+ /**
+ * Resolves a relative path (separated by '/') to a file. Besides returning a File, this method is identical to {@link #resolveFile(Folder, String)}.
+ *
+ * @param dir The directory from which to resolve the path.
+ * @param relativePath The path relative to a given directory.
+ * @return The file with the given path relative to the given dir.
+ */
+ public static File resolveFile(Folder dir, String relativePath) {
+ final String[] fragments = StringUtils.split(relativePath, '/');
+ if (ArrayUtils.isEmpty(fragments)) {
+ throw new IllegalArgumentException("Empty relativePath");
+ }
+ final Folder folder = resolveFolder(dir, Arrays.stream(fragments).limit(fragments.length - 1).iterator());
+ final String filename = fragments[fragments.length - 1];
+ return folder.file(filename);
+ }
+
+ private static Folder resolveFolder(Folder dir, Iterator remainingPathFragments) {
+ if (!remainingPathFragments.hasNext()) {
+ return dir;
+ }
+ final String fragment = remainingPathFragments.next();
+ assert fragment.length() > 0 : "iterator must not contain empty fragments";
+ if (DOT.equals(fragment)) {
+ return resolveFolder(dir, remainingPathFragments);
+ } else if (DOTDOT.equals(fragment) && dir.parent().isPresent()) {
+ return resolveFolder(dir.parent().get(), remainingPathFragments);
+ } else if (DOTDOT.equals(fragment) && !dir.parent().isPresent()) {
+ throw new UncheckedIOException(new FileNotFoundException("Unresolvable path"));
+ } else {
+ return resolveFolder(dir.folder(fragment), remainingPathFragments);
+ }
+ }
+
+}
diff --git a/main/filesystem-api/src/test/java/org/cryptomator/filesystem/PathResolverTest.java b/main/filesystem-api/src/test/java/org/cryptomator/filesystem/PathResolverTest.java
new file mode 100644
index 000000000..527307010
--- /dev/null
+++ b/main/filesystem-api/src/test/java/org/cryptomator/filesystem/PathResolverTest.java
@@ -0,0 +1,68 @@
+package org.cryptomator.filesystem;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Optional;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class PathResolverTest {
+
+ private final Folder root = Mockito.mock(Folder.class);
+ private final Folder foo = Mockito.mock(Folder.class);
+ private final Folder bar = Mockito.mock(Folder.class);
+ private final File baz = Mockito.mock(File.class);
+
+ @Before
+ public void configureMocks() throws IOException {
+ Mockito.doReturn(Optional.empty()).when(root).parent();
+ Mockito.doReturn(Optional.of(root)).when(foo).parent();
+ Mockito.doReturn(Optional.of(foo)).when(bar).parent();
+
+ Mockito.doReturn(foo).when(root).folder("foo");
+ Mockito.doReturn(bar).when(foo).folder("bar");
+ Mockito.doReturn(baz).when(bar).file("baz");
+ }
+
+ @Test
+ public void testResolveChildFolder() {
+ Assert.assertEquals(bar, PathResolver.resolveFolder(root, "foo/bar"));
+ Assert.assertEquals(bar, PathResolver.resolveFolder(root, "foo/./bar"));
+ Assert.assertEquals(bar, PathResolver.resolveFolder(root, "./foo/././bar"));
+ }
+
+ @Test
+ public void testResolveParentFolder() {
+ Assert.assertEquals(foo, PathResolver.resolveFolder(bar, ".."));
+ Assert.assertEquals(root, PathResolver.resolveFolder(bar, "../.."));
+ }
+
+ @Test
+ public void testResolveSiblingFolder() {
+ Assert.assertEquals(foo, PathResolver.resolveFolder(bar, "../../foo"));
+ }
+
+ @Test(expected = UncheckedIOException.class)
+ public void testResolveUnresolvableFolder() {
+ PathResolver.resolveFolder(root, "..");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testResolveFolderWithEmptyPath() {
+ PathResolver.resolveFolder(root, "");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testResolveFileWithEmptyPath() {
+ PathResolver.resolveFile(root, "");
+ }
+
+ @Test
+ public void testResolveFile() {
+ Assert.assertEquals(baz, PathResolver.resolveFile(foo, "../foo/bar/./baz"));
+ }
+
+}
diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
index 22dc06ec9..26ec3d2e0 100644
--- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
+++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
@@ -16,6 +16,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
+import org.apache.commons.io.FileExistsException;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
@@ -45,10 +46,12 @@ class InMemoryFile extends InMemoryNode implements File {
writeLock.lock();
final InMemoryFolder parent = parent().get();
parent.children.compute(this.name(), (k, v) -> {
- if (v != null && v != this) {
- throw new IllegalStateException("More than one representation of same file");
+ if (v == null || v == this) {
+ this.lastModified = Instant.now();
+ return this;
+ } else {
+ throw new UncheckedIOException(new FileExistsException(k));
}
- return this;
});
return new InMemoryWritableFile(this::setLastModified, this::getContent, this::setContent, this::delete, writeLock);
}
diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java
index 9a2225699..5074c015d 100644
--- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java
+++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java
@@ -9,7 +9,6 @@
package org.cryptomator.filesystem.inmem;
import java.io.UncheckedIOException;
-import java.nio.file.FileAlreadyExistsException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Iterator;
@@ -36,31 +35,21 @@ class InMemoryFolder extends InMemoryNode implements Folder {
@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);
- });
- }
+ final InMemoryNode node = children.get(name);
if (node instanceof InMemoryFile) {
return (InMemoryFile) node;
} else {
- throw new UncheckedIOException(new FileAlreadyExistsException(name + " exists, but is not a file."));
+ return new InMemoryFile(this, name, Instant.MIN);
}
}
@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);
- });
- }
+ final InMemoryNode node = children.get(name);
if (node instanceof InMemoryFolder) {
return (InMemoryFolder) node;
} else {
- throw new UncheckedIOException(new FileAlreadyExistsException(name + " exists, but is not a folder."));
+ return new InMemoryFolder(this, name, Instant.MIN);
}
}
@@ -86,11 +75,11 @@ class InMemoryFolder extends InMemoryNode implements Folder {
if (target.exists()) {
target.delete();
}
- assert !target.exists();
+ assert!target.exists();
target.create();
this.copyTo(target);
this.delete();
- assert !this.exists();
+ assert!this.exists();
}
@Override
@@ -112,7 +101,7 @@ class InMemoryFolder extends InMemoryNode implements Folder {
subFolder.delete();
}
}
- assert !this.exists();
+ assert!this.exists();
}
@Override