diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Mover.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Mover.java
index 454dbf188..e01cdf3e1 100644
--- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Mover.java
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Mover.java
@@ -11,6 +11,9 @@ package org.cryptomator.filesystem;
class Mover {
public static void move(File source, File destination) {
+ if (source == destination) {
+ return;
+ }
try (OpenFiles openFiles = DeadlockSafeFileOpener.withWritable(source).andWritable(destination).open()) {
openFiles.writable(source).moveTo(openFiles.writable(destination));
}
diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/WritableFile.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/WritableFile.java
index 1d2282093..9e44eb8a8 100644
--- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/WritableFile.java
+++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/WritableFile.java
@@ -13,6 +13,13 @@ import java.time.Instant;
public interface WritableFile extends WritableByteChannel {
+ /**
+ *
+ * Moves this file including content to another.
+ *
+ * Moving a file causes itself and the target to be
+ * {@link WritableFile#close() closed}.
+ */
void moveTo(WritableFile other) throws UncheckedIOException;
void setLastModified(Instant instant) throws UncheckedIOException;
diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java
index 335bb67f0..2df9931d5 100644
--- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java
+++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java
@@ -4,7 +4,6 @@ import static java.lang.String.format;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
@@ -18,101 +17,56 @@ import org.cryptomator.filesystem.WritableFile;
class NioFile extends NioNode implements File {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
+ private SharedFileChannel sharedChannel;
public NioFile(Optional parent, Path path) {
super(parent, path);
+ sharedChannel = new SharedFileChannel(path);
+ }
+
+ SharedFileChannel channel() {
+ return sharedChannel;
+ }
+
+ public ReentrantReadWriteLock lock() {
+ return lock;
}
@Override
public ReadableFile openReadable() throws UncheckedIOException {
if (lock.getWriteHoldCount() > 0) {
- throw new IllegalStateException("Current thread is currently reading this file");
+ throw new IllegalStateException("Current thread is currently writing this file");
+ }
+ if (lock.getReadHoldCount() > 0) {
+ throw new IllegalStateException("Current thread is already reading this file");
}
lock.readLock().lock();
- return new ReadableView();
+ return new ReadableNioFile(this);
}
@Override
public WritableFile openWritable() throws UncheckedIOException {
+ if (lock.getWriteHoldCount() > 0) {
+ throw new IllegalStateException("Current thread is already writing this file");
+ }
if (lock.getReadHoldCount() > 0) {
throw new IllegalStateException("Current thread is currently reading this file");
}
- lock.readLock().lock();
- return new WritableView();
+ lock.writeLock().lock();
+ return new WritableNioFile(this);
}
@Override
public boolean exists() throws UncheckedIOException {
- return false;
+ return Files.isRegularFile(path);
}
- private class ReadableView implements ReadableFile {
-
- @Override
- public int read(ByteBuffer target) throws UncheckedIOException {
- return -1;
+ @Override
+ public Instant lastModified() throws UncheckedIOException {
+ if (Files.exists(path) && !exists()) {
+ throw new UncheckedIOException(new IOException(format("%s is a folder", path)));
}
-
- @Override
- public boolean isOpen() {
- return false;
- }
-
- @Override
- public void position(long position) throws UncheckedIOException {
- }
-
- @Override
- public void copyTo(WritableFile other) throws UncheckedIOException {
- }
-
- @Override
- public void close() throws UncheckedIOException {
- }
-
- }
-
- private class WritableView implements WritableFile {
-
- @Override
- public int write(ByteBuffer source) throws UncheckedIOException {
- return -1;
- }
-
- @Override
- public boolean isOpen() {
- return false;
- }
-
- @Override
- public void position(long position) throws UncheckedIOException {
- }
-
- @Override
- public void moveTo(WritableFile other) throws UncheckedIOException {
- }
-
- @Override
- public void setLastModified(Instant instant) throws UncheckedIOException {
- }
-
- @Override
- public void delete() throws UncheckedIOException {
- try {
- Files.delete(path);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- @Override
- public void truncate() throws UncheckedIOException {
- }
-
- @Override
- public void close() throws UncheckedIOException {
- }
-
+ return super.lastModified();
}
@Override
diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/OpenMode.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/OpenMode.java
new file mode 100644
index 000000000..a0616839f
--- /dev/null
+++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/OpenMode.java
@@ -0,0 +1,5 @@
+package org.cryptomator.filesystem.nio;
+
+enum OpenMode {
+ READ, WRITE
+}
diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/ReadableNioFile.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/ReadableNioFile.java
new file mode 100644
index 000000000..ce6ee910f
--- /dev/null
+++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/ReadableNioFile.java
@@ -0,0 +1,95 @@
+package org.cryptomator.filesystem.nio;
+
+import static java.lang.String.format;
+import static org.cryptomator.filesystem.nio.OpenMode.READ;
+
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+
+class ReadableNioFile implements ReadableFile {
+
+ private final NioFile nioFile;
+
+ private boolean open = true;
+ private long position = 0;
+
+ public ReadableNioFile(NioFile nioFile) {
+ this.nioFile = nioFile;
+ nioFile.channel().open(READ);
+ }
+
+ @Override
+ public int read(ByteBuffer target) throws UncheckedIOException {
+ assertOpen();
+ int read = nioFile.channel().readFully(position, target);
+ if (read != SharedFileChannel.EOF) {
+ position += read;
+ }
+ return read;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return open;
+ }
+
+ @Override
+ public void position(long position) throws UncheckedIOException {
+ assertOpen();
+ this.position = position;
+ }
+
+ @Override
+ public void copyTo(WritableFile other) throws UncheckedIOException {
+ assertOpen();
+ if (belongsToSameFilesystem(other)) {
+ internalCopyTo((WritableNioFile) other);
+ } else {
+ throw new IllegalArgumentException("Can only copy to a WritableFile from the same FileSystem");
+ }
+ }
+
+ private boolean belongsToSameFilesystem(WritableFile other) {
+ return other instanceof WritableNioFile && ((WritableNioFile) other).nioFile().belongsToSameFilesystem(nioFile);
+ }
+
+ private void internalCopyTo(WritableNioFile target) {
+ target.ensureChannelIsOpened();
+ SharedFileChannel targetChannel = target.channel();
+ targetChannel.truncate(0);
+ long size = nioFile.channel().size();
+ long transferred = 0;
+ while (transferred < size) {
+ transferred += nioFile.channel().transferTo(transferred, size - transferred, targetChannel);
+ }
+ }
+
+ @Override
+ public void close() {
+ if (!open) {
+ return;
+ }
+ open = false;
+ try {
+ nioFile.channel().close();
+ } finally {
+ nioFile.lock().readLock().unlock();
+ }
+ }
+
+ private void assertOpen() {
+ if (!open) {
+ throw new UncheckedIOException(format("%s already closed.", this), new ClosedChannelException());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return format("Readable%s", nioFile);
+ }
+
+}
\ No newline at end of file
diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/SharedFileChannel.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/SharedFileChannel.java
new file mode 100644
index 000000000..54aac6c37
--- /dev/null
+++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/SharedFileChannel.java
@@ -0,0 +1,167 @@
+package org.cryptomator.filesystem.nio;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+class SharedFileChannel {
+
+ public static final int EOF = -1;
+
+ private final Path path;
+
+ private Map openedBy = new ConcurrentHashMap<>();
+ private Lock lock = new ReentrantLock();
+
+ private FileChannel delegate;
+
+ public SharedFileChannel(Path path) {
+ this.path = path;
+ }
+
+ public void open(OpenMode mode) {
+ doLocked(() -> {
+ Thread thread = Thread.currentThread();
+ if (openedBy.put(thread, thread) != null) {
+ throw new IllegalStateException("A thread can only open a SharedFileChannel once");
+ }
+ if (delegate == null) {
+ createChannel(mode);
+ }
+ });
+ }
+
+ public void close() {
+ assertOpenedByCurrentThread();
+ doLocked(() -> {
+ openedBy.remove(Thread.currentThread());
+ if (openedBy.isEmpty()) {
+ closeChannel();
+ }
+ });
+ }
+
+ private void assertOpenedByCurrentThread() {
+ if (!openedBy.containsKey(Thread.currentThread())) {
+ throw new IllegalStateException("SharedFileChannel closed for current thread");
+ }
+ }
+
+ private void createChannel(OpenMode mode) {
+ try {
+ FileChannel readChannel = null;
+ if (mode == OpenMode.READ) {
+ readChannel = FileChannel.open(path, StandardOpenOption.READ);
+ }
+ delegate = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
+ if (readChannel != null) {
+ readChannel.close();
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private void closeChannel() {
+ try {
+ delegate.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } finally {
+ delegate = null;
+ }
+ }
+
+ public int readFully(long position, ByteBuffer target) {
+ assertOpenedByCurrentThread();
+ try {
+ return tryReadFully(position, target);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private int tryReadFully(long position, ByteBuffer target) throws IOException {
+ int initialRemaining = target.remaining();
+ long maxPosition = position + initialRemaining;
+ do {
+ if (delegate.read(target, maxPosition - target.remaining()) == EOF) {
+ if (initialRemaining == target.remaining()) {
+ return EOF;
+ } else {
+ return initialRemaining - target.remaining();
+ }
+ }
+ } while (target.hasRemaining());
+ return initialRemaining - target.remaining();
+ }
+
+ public void truncate(int i) {
+ assertOpenedByCurrentThread();
+ try {
+ delegate.truncate(i);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public long size() {
+ assertOpenedByCurrentThread();
+ try {
+ return delegate.size();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public long transferTo(long position, long count, SharedFileChannel targetChannel) {
+ assertOpenedByCurrentThread();
+ targetChannel.assertOpenedByCurrentThread();
+ try {
+ long maxPosition = delegate.size();
+ long maxCount = Math.min(count, maxPosition - position);
+ long remaining = maxCount;
+ while (remaining > 0) {
+ remaining -= delegate.transferTo(maxPosition - remaining, remaining, targetChannel.delegate);
+ }
+ return maxCount;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private void doLocked(Runnable task) {
+ lock.lock();
+ try {
+ task.run();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public int writeFully(long position, ByteBuffer source) {
+ assertOpenedByCurrentThread();
+ try {
+ return tryWriteFully(position, source);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private int tryWriteFully(long position, ByteBuffer source) throws IOException {
+ int initialRemaining = source.remaining();
+ long maxPosition = position + initialRemaining;
+ do {
+ delegate.write(source, maxPosition - source.remaining());
+ } while (source.hasRemaining());
+ return initialRemaining - source.remaining();
+ }
+
+}
diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/WritableNioFile.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/WritableNioFile.java
new file mode 100644
index 000000000..0347c1470
--- /dev/null
+++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/WritableNioFile.java
@@ -0,0 +1,173 @@
+package org.cryptomator.filesystem.nio;
+
+import static java.lang.String.format;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static org.cryptomator.filesystem.nio.OpenMode.WRITE;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+
+import org.cryptomator.filesystem.WritableFile;
+
+class WritableNioFile implements WritableFile {
+
+ private final NioFile nioFile;
+
+ private boolean channelOpened = false;
+ private boolean open = true;
+ private long position = 0;
+
+ public WritableNioFile(NioFile nioFile) {
+ this.nioFile = nioFile;
+ }
+
+ @Override
+ public int write(ByteBuffer source) throws UncheckedIOException {
+ assertOpen();
+ ensureChannelIsOpened();
+ int written = nioFile.channel().writeFully(position, source);
+ position += written;
+ return written;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return open;
+ }
+
+ @Override
+ public void position(long position) throws UncheckedIOException {
+ assertOpen();
+ this.position = position;
+ }
+
+ private boolean belongsToSameFilesystem(WritableFile other) {
+ return other instanceof WritableNioFile && ((WritableNioFile) other).nioFile().belongsToSameFilesystem(nioFile);
+ }
+
+ @Override
+ public void moveTo(WritableFile other) throws UncheckedIOException {
+ assertOpen();
+ if (other == this) {
+ return;
+ } else if (belongsToSameFilesystem(other)) {
+ internalMoveTo((WritableNioFile) other);
+ } else {
+ throw new IllegalArgumentException("Can only move to a WritableFile from the same FileSystem");
+ }
+ }
+
+ private void internalMoveTo(WritableNioFile other) {
+ other.assertOpen();
+ try {
+ assertMovePreconditionsAreMet(other);
+ closeChannelIfOpened();
+ other.closeChannelIfOpened();
+ Files.move(path(), other.path(), REPLACE_EXISTING);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } finally {
+ open = false;
+ other.open = false;
+ other.nioFile.lock().writeLock().unlock();
+ nioFile.lock().writeLock().unlock();
+ }
+ }
+
+ private void assertMovePreconditionsAreMet(WritableNioFile other) {
+ if (Files.isDirectory(path())) {
+ throw new UncheckedIOException(new IOException(format("Can not move %s to %s. Source is a directory", path(), other.path())));
+ }
+ if (Files.isDirectory(other.path())) {
+ throw new UncheckedIOException(new IOException(format("Can not move %s to %s. Target is a directory", path(), other.path())));
+ }
+ }
+
+ @Override
+ public void setLastModified(Instant instant) throws UncheckedIOException {
+ assertOpen();
+ ensureChannelIsOpened();
+ try {
+ Files.setLastModifiedTime(path(), FileTime.from(instant));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public void delete() throws UncheckedIOException {
+ assertOpen();
+ try {
+ closeChannelIfOpened();
+ Files.delete(nioFile.path);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } finally {
+ open = false;
+ nioFile.lock().writeLock().unlock();
+ }
+ }
+
+ @Override
+ public void truncate() throws UncheckedIOException {
+ assertOpen();
+ ensureChannelIsOpened();
+ nioFile.channel().truncate(0);
+ }
+
+ @Override
+ public void close() throws UncheckedIOException {
+ if (!open) {
+ return;
+ }
+ open = false;
+ try {
+ closeChannelIfOpened();
+ } finally {
+ nioFile.lock().writeLock().unlock();
+ }
+ }
+
+ void ensureChannelIsOpened() {
+ if (!channelOpened) {
+ nioFile.channel().open(WRITE);
+ channelOpened = true;
+ }
+ }
+
+ private void closeChannelIfOpened() {
+ if (channelOpened) {
+ channel().close();
+ }
+ }
+
+ SharedFileChannel channel() {
+ return nioFile.channel();
+ }
+
+ Path path() {
+ return nioFile.path;
+ }
+
+ public NioFile nioFile() {
+ return nioFile;
+ }
+
+ private void assertOpen() {
+ if (!open) {
+ throw new UncheckedIOException(format("%s already closed.", this), new ClosedChannelException());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return format("Writable%s", this.nioFile);
+ }
+
+}
\ No newline at end of file
diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/FilesystemSetupUtils.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/FilesystemSetupUtils.java
index b8f8c27d5..dac4f57df 100644
--- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/FilesystemSetupUtils.java
+++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/FilesystemSetupUtils.java
@@ -51,6 +51,7 @@ class FilesystemSetupUtils {
public static class FileEntry implements Entry {
private Path relativePath;
private byte[] data = new byte[0];
+ private Instant lastModified;
public FileEntry(Path relativePath) {
this.relativePath = relativePath;
@@ -65,6 +66,11 @@ class FilesystemSetupUtils {
return withData(data.getBytes());
}
+ public FileEntry withLastModified(Instant lastModified) {
+ this.lastModified = lastModified;
+ return this;
+ }
+
@Override
public void create(Path root) throws IOException {
Path filePath = root.resolve(relativePath);
@@ -72,6 +78,9 @@ class FilesystemSetupUtils {
try (OutputStream out = Files.newOutputStream(filePath)) {
IOUtils.write(data, out);
}
+ if (lastModified != null) {
+ Files.setLastModifiedTime(filePath, FileTime.from(lastModified));
+ }
}
}
diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java
new file mode 100644
index 000000000..7f6212781
--- /dev/null
+++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java
@@ -0,0 +1,352 @@
+package org.cryptomator.filesystem.nio;
+
+import static java.lang.String.format;
+import static org.cryptomator.common.test.matcher.OptionalMatcher.presentOptionalWithValueThat;
+import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.emptyFilesystem;
+import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.file;
+import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.folder;
+import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.testFilesystem;
+import static org.cryptomator.filesystem.nio.PathMatcher.doesNotExist;
+import static org.cryptomator.filesystem.nio.PathMatcher.isFile;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.junit.Assert.assertThat;
+
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.time.Instant;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.Folder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import de.bechte.junit.runners.context.HierarchicalContextRunner;
+
+@RunWith(HierarchicalContextRunner.class)
+public class NioFileTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testExistsForExistingFileReturnsTrue() {
+ File existingFile = NioFileSystem.rootedAt(testFilesystem(file("testFile"))) //
+ .file("testFile");
+
+ assertThat(existingFile.exists(), is(true));
+ }
+
+ @Test
+ public void testExistsForNonExistingFileReturnsFalse() {
+ File nonExistingFile = NioFileSystem.rootedAt(emptyFilesystem()) //
+ .file("testFile");
+
+ assertThat(nonExistingFile.exists(), is(false));
+ }
+
+ @Test
+ public void testExistsForFileWhichIsAFolderReturnsFalse() {
+ File fileWhichIsAFolder = NioFileSystem.rootedAt(testFilesystem(folder("nameOfAnExistingFolder"))) //
+ .file("nameOfAnExistingFolder");
+
+ assertThat(fileWhichIsAFolder.exists(), is(false));
+ }
+
+ @Test
+ public void testLastModifiedForExistingFileReturnsLastModifiedValue() {
+ Instant expectedLastModified = Instant.parse("2015-12-31T15:03:34Z");
+ File existingFile = NioFileSystem
+ .rootedAt(testFilesystem( //
+ file("testFile").withLastModified(expectedLastModified))) //
+ .file("testFile");
+
+ assertThat(existingFile.lastModified(), is(expectedLastModified));
+ }
+
+ @Test
+ public void testLastModifiedForNonExistingFileThrowsUncheckedIOExceptionWithPathInMessage() {
+ Path filesystemPath = emptyFilesystem();
+ Path pathOfNonExistingFile = filesystemPath.resolve("nonExistingFile");
+ File nonExistingFile = NioFileSystem.rootedAt(filesystemPath) //
+ .file("nonExistingFile");
+
+ thrown.expect(UncheckedIOException.class);
+ thrown.expectMessage(pathOfNonExistingFile.toString());
+
+ nonExistingFile.lastModified();
+ }
+
+ @Test
+ public void testLastModifiedForNonFileWhichIsAFolderThrowsUncheckedIOExceptionWithPathInMessage() {
+ Path filesystemPath = testFilesystem(folder("nameOfAnExistingFolder"));
+ Path pathOfNonExistingFile = filesystemPath.resolve("nameOfAnExistingFolder");
+ File fileWhichIsAFolder = NioFileSystem.rootedAt(filesystemPath) //
+ .file("nameOfAnExistingFolder");
+
+ thrown.expect(UncheckedIOException.class);
+ thrown.expectMessage(pathOfNonExistingFile.toString());
+
+ fileWhichIsAFolder.lastModified();
+ }
+
+ @Test
+ public void testCompareToReturnsZeroForSameInstance() {
+ File file = NioFileSystem.rootedAt(emptyFilesystem()).file("fileName");
+
+ assertThat(file.compareTo(file), is(0));
+ }
+
+ @Test
+ public void testCompareToReturnsZeroForSameFile() {
+ FileSystem filesystem = NioFileSystem.rootedAt(emptyFilesystem());
+ File fileA = filesystem.file("fileName");
+ File fileB = filesystem.file("fileName");
+
+ assertThat(fileA.compareTo(fileB), is(0));
+ assertThat(fileB.compareTo(fileA), is(0));
+ }
+
+ @Test
+ public void testCompareToReturnsNonZeroForOtherFile() {
+ FileSystem filesystem = NioFileSystem.rootedAt(emptyFilesystem());
+ File fileA = filesystem.file("aFileName");
+ File fileB = filesystem.file("anotherFileName");
+
+ int compareAWithB = fileA.compareTo(fileB);
+ int compareBWithA = fileB.compareTo(fileA);
+ assertThat(compareAWithB, not(is(0)));
+ assertThat(compareBWithA, not(is(0)));
+ assertThat(signum(compareAWithB) + signum(compareBWithA), is(0));
+ }
+
+ @Test
+ public void testCompareToThrowsExceptionForFileFromDifferentFileSystem() {
+ File fileA = NioFileSystem.rootedAt(emptyFilesystem()).file("aFileName");
+ File fileB = NioFileSystem.rootedAt(emptyFilesystem()).file("aFileName");
+
+ thrown.expect(IllegalArgumentException.class);
+
+ fileA.compareTo(fileB);
+ }
+
+ @Test
+ public void testToString() {
+ Path filesystemPath = emptyFilesystem();
+ Path absoluteFilePath = filesystemPath.resolve("fileName").toAbsolutePath();
+ File file = NioFileSystem.rootedAt(filesystemPath).file("fileName");
+
+ assertThat(file.toString(), is(format("NioFile(%s)", absoluteFilePath)));
+ }
+
+ @Test
+ public void testNameReturnsNameOfFile() {
+ String fileName = "fileName";
+ File file = NioFileSystem.rootedAt(emptyFilesystem()).file(fileName);
+
+ assertThat(file.name(), is(fileName));
+ }
+
+ @Test
+ public void testParentForDirectChildOfFileSystemReturnsFileSystem() {
+ FileSystem fileSystem = NioFileSystem.rootedAt(emptyFilesystem());
+ File file = fileSystem.file("fileName");
+
+ assertThat(file.parent(), presentOptionalWithValueThat(is(sameInstance(fileSystem))));
+ }
+
+ @Test
+ public void testParentForChildOfFolderReturnsFolder() {
+ Folder folder = NioFileSystem.rootedAt(emptyFilesystem()).folder("folderName");
+ File file = folder.file("fileName");
+
+ assertThat(file.parent(), presentOptionalWithValueThat(is(sameInstance(folder))));
+ }
+
+ @Test
+ public void testCopyToNonExistingTargetCreatesTargetWithContent() {
+ Path filesystemPath = testFilesystem(file("sourceFile").withData("fileContents"));
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ Path sourceFilePath = filesystemPath.resolve("sourceFile");
+ Path targetFilePath = filesystemPath.resolve("targetFile");
+ File source = fileSystem.file("sourceFile");
+ File target = fileSystem.file("targetFile");
+
+ source.copyTo(target);
+
+ assertThat(sourceFilePath, isFile().withContent("fileContents"));
+ assertThat(targetFilePath, isFile().withContent("fileContents"));
+ }
+
+ @Test
+ public void testCopyToExistingTargetOverwritesTargetWithContent() {
+ Path filesystemPath = testFilesystem( //
+ file("sourceFile").withData("fileContents"), //
+ file("targetFile").withData("wrongFileContents"));
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ Path sourceFilePath = filesystemPath.resolve("sourceFile");
+ Path targetFilePath = filesystemPath.resolve("targetFile");
+ File source = fileSystem.file("sourceFile");
+ File target = fileSystem.file("targetFile");
+
+ source.copyTo(target);
+
+ assertThat(sourceFilePath, isFile().withContent("fileContents"));
+ assertThat(targetFilePath, isFile().withContent("fileContents"));
+ }
+
+ @Test
+ public void testCopyToSameFileThrowsIllegalArgumentException() {
+ File file = NioFileSystem.rootedAt(testFilesystem(file("sourceFile"))).file("fileName");
+
+ thrown.expect(IllegalArgumentException.class);
+
+ file.copyTo(file);
+ }
+
+ @Test
+ public void testCopyToDirectoryTargetThrowsUncheckedIOExceptionWithPathInMessage() {
+ Path filesystemPath = testFilesystem( //
+ file("sourceFile").withData("fileContents"), //
+ folder("aFolderName"));
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ Path targetFilePath = filesystemPath.resolve("aFolderName").toAbsolutePath();
+ File source = fileSystem.file("sourceFile");
+ File target = fileSystem.file("aFolderName");
+
+ thrown.expect(UncheckedIOException.class);
+ thrown.expectMessage(targetFilePath.toAbsolutePath().toString());
+
+ source.copyTo(target);
+ }
+
+ @Test
+ public void testCopyToOfNonExistingFileThrowsUncheckedIOExceptionWithPathInMessage() {
+ Path filesystemPath = emptyFilesystem();
+ Path filePath = filesystemPath.resolve("nonExistingFile").toAbsolutePath();
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ File nonExistingFile = fileSystem.file("nonExistingFile");
+ File target = fileSystem.file("target");
+
+ thrown.expect(UncheckedIOException.class);
+ thrown.expectMessage(filePath.toString());
+
+ nonExistingFile.copyTo(target);
+ }
+
+ @Test
+ public void testCopyToOfFileWhichIsAFolderThrowsUncheckedIOExceptionWithPathInMessage() {
+ Path filesystemPath = testFilesystem(folder("folderName"));
+ Path filePath = filesystemPath.resolve("folderName").toAbsolutePath();
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ File fileWhichIsAFolder = fileSystem.file("folderName");
+ File target = fileSystem.file("target");
+
+ thrown.expect(UncheckedIOException.class);
+ thrown.expectMessage(filePath.toString());
+
+ fileWhichIsAFolder.copyTo(target);
+ }
+
+ @Test
+ public void testMoveToNonExistingTargetCreatesTargetWithContentAndDeletesSource() {
+ Path filesystemPath = testFilesystem(file("sourceFile").withData("fileContents"));
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ Path sourceFilePath = filesystemPath.resolve("sourceFile");
+ Path targetFilePath = filesystemPath.resolve("targetFile");
+ File source = fileSystem.file("sourceFile");
+ File target = fileSystem.file("targetFile");
+
+ source.moveTo(target);
+
+ assertThat(sourceFilePath, doesNotExist());
+ assertThat(targetFilePath, isFile().withContent("fileContents"));
+ }
+
+ @Test
+ public void testMoveToExistingTargetOverwritesTargetWithContentAndDeletesSource() {
+ Path filesystemPath = testFilesystem( //
+ file("sourceFile").withData("fileContents"), //
+ file("targetFile").withData("wrongFileContents"));
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ Path sourceFilePath = filesystemPath.resolve("sourceFile");
+ Path targetFilePath = filesystemPath.resolve("targetFile");
+ File source = fileSystem.file("sourceFile");
+ File target = fileSystem.file("targetFile");
+
+ source.moveTo(target);
+
+ assertThat(sourceFilePath, doesNotExist());
+ assertThat(targetFilePath, isFile().withContent("fileContents"));
+ }
+
+ @Test
+ public void testMoveToSameFileDoesNothing() {
+ Path filesystemPath = testFilesystem(file("fileName").withData("fileContents"));
+ Path filePath = filesystemPath.resolve("fileName");
+ File file = NioFileSystem.rootedAt(filesystemPath).file("fileName");
+
+ file.moveTo(file);
+
+ assertThat(filePath, isFile().withContent("fileContents"));
+ }
+
+ @Test
+ public void testMoveToDirectoryTargetThrowsUncheckedIOExceptionWithPathInMessage() {
+ Path filesystemPath = testFilesystem( //
+ file("sourceFile").withData("fileContents"), //
+ folder("aFolderName"));
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ Path targetFilePath = filesystemPath.resolve("aFolderName").toAbsolutePath();
+ File source = fileSystem.file("sourceFile");
+ File target = fileSystem.file("aFolderName");
+
+ thrown.expect(UncheckedIOException.class);
+ thrown.expectMessage(targetFilePath.toAbsolutePath().toString());
+
+ source.moveTo(target);
+ }
+
+ @Test
+ public void testMoveToOfNonExistingFileThrowsUncheckedIOExceptionWithPathInMessage() {
+ Path filesystemPath = emptyFilesystem();
+ Path filePath = filesystemPath.resolve("nonExistingFile").toAbsolutePath();
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ File nonExistingFile = fileSystem.file("nonExistingFile");
+ File target = fileSystem.file("target");
+
+ thrown.expect(UncheckedIOException.class);
+ thrown.expectMessage(filePath.toString());
+
+ nonExistingFile.moveTo(target);
+ }
+
+ @Test
+ public void testMoveToOfFileWhichIsAFolderThrowsUncheckedIOExceptionWithPathInMessage() {
+ Path filesystemPath = testFilesystem(folder("folderName"));
+ Path filePath = filesystemPath.resolve("folderName").toAbsolutePath();
+ FileSystem fileSystem = NioFileSystem.rootedAt(filesystemPath);
+ File fileWhichIsAFolder = fileSystem.file("folderName");
+ File target = fileSystem.file("target");
+
+ thrown.expect(UncheckedIOException.class);
+ thrown.expectMessage(filePath.toString());
+
+ fileWhichIsAFolder.moveTo(target);
+ }
+
+ private int signum(int value) {
+ if (value > 0) {
+ return 1;
+ } else if (value < 0) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+
+}
diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFolderTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFolderTest.java
index 4b383541e..3ef5acedf 100644
--- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFolderTest.java
+++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFolderTest.java
@@ -32,6 +32,7 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
public class NioFolderTest {
+
@Rule
public ExpectedException thrown = ExpectedException.none();