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();