ソースを参照

adjusted in-memory filesystem to comply with API (return files/folders when requested, even though the oposite kind exists for the given name)

Sebastian Stenzel 9 年 前
コミット
4e7f3503d9

+ 20 - 0
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);
+	}
+
 	/**
 	 * <p>
 	 * 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.

+ 100 - 0
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.
+	 * <!-- @formatter:off -->
+	 * <table>
+	 * <thead>
+	 * <tr>
+	 * <th>dir</th>
+	 * <th>path</th>
+	 * <th>result</th>
+	 * </tr>
+	 * </thead>
+	 * <tbody>
+	 * <tr>
+	 * <td>/foo/bar</td>
+	 * <td>foo/bar</td>
+	 * <td>/foo/bar/foo/bar</td>
+	 * </tr>
+	 * <tr>
+	 * <td>/foo/bar</td>
+	 * <td>../baz</td>
+	 * <td>/foo/baz</td>
+	 * </tr>
+	 * <tr>
+	 * <td>/foo/bar</td>
+	 * <td>./foo/..</td>
+	 * <td>/foo/bar</td>
+	 * </tr>
+	 * <tr>
+	 * <td>/foo/bar</td>
+	 * <td>../../..</td>
+	 * <td>Exception</td>
+	 * </tr>
+	 * </tbody>
+	 * </table>
+	 * 
+	 * @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<String> 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);
+		}
+	}
+
+}

+ 68 - 0
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"));
+	}
+
+}

+ 6 - 3
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);
 	}

+ 7 - 18
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