mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-17 18:21:26 +00:00
adjusted in-memory filesystem to comply with API (return files/folders when requested, even though the oposite kind exists for the given name)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user