From 4e7f3503d9770f3b85f619494c35cb0621be2e46 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 1 Jan 2016 22:44:46 +0100 Subject: [PATCH] adjusted in-memory filesystem to comply with API (return files/folders when requested, even though the oposite kind exists for the given name) --- .../org/cryptomator/filesystem/Folder.java | 20 ++++ .../cryptomator/filesystem/PathResolver.java | 100 ++++++++++++++++++ .../filesystem/PathResolverTest.java | 68 ++++++++++++ .../filesystem/inmem/InMemoryFile.java | 9 +- .../filesystem/inmem/InMemoryFolder.java | 25 ++--- 5 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 main/filesystem-api/src/main/java/org/cryptomator/filesystem/PathResolver.java create mode 100644 main/filesystem-api/src/test/java/org/cryptomator/filesystem/PathResolverTest.java 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. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
dirpathresult
/foo/barfoo/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