diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java index 72f4db22c..45c1f0d90 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java @@ -77,7 +77,16 @@ public interface File extends Node, Comparable { Mover.move(this, destination); } + /** + *

+ * Deletes the file if it exists. + *

+ * Does nothign if the file does not exist. + */ default void delete() { + if (!exists()) { + return; + } try (WritableFile writableFile = openWritable()) { writableFile.delete(); } diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java index dc85711b8..4dd90fd14 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java @@ -46,6 +46,9 @@ class InMemoryNode implements Node { @Override public Instant lastModified() { + if (!exists()) { + throw new UncheckedIOException(new IOException("File does not exist")); + } return lastModified; } diff --git a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileReadWriteTests.java b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileReadWriteTests.java new file mode 100644 index 000000000..5999b35e1 --- /dev/null +++ b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileReadWriteTests.java @@ -0,0 +1,133 @@ +package org.cryptomator.filesystem.invariants; + +import static org.cryptomator.filesystem.invariants.matchers.NodeMatchers.hasContent; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +import java.nio.ByteBuffer; + +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.WritableFile; +import org.cryptomator.filesystem.invariants.FileSystemFactories.FileSystemFactory; +import org.cryptomator.filesystem.invariants.WaysToObtainAFile.WayToObtainAFile; +import org.cryptomator.filesystem.invariants.WaysToObtainAFolder.WayToObtainAFolder; +import org.junit.Rule; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +@RunWith(Theories.class) +public class FileReadWriteTests { + + @DataPoints + public static final Iterable FILE_SYSTEM_FACTORIES = new FileSystemFactories(); + + @DataPoints + public static final Iterable WAYS_TO_OBTAIN_A_FOLDER = new WaysToObtainAFolder(); + + @DataPoints + public static final Iterable WAYS_TO_OBTAIN_A_FILE = new WaysToObtainAFile(); + + private static final String FILE_NAME = "fileName"; + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Theory + public void testWriteToNonExistingFileCreatesFileWithContent(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainANonExistingFile) { + assumeThat(wayToObtainANonExistingFile.returnedFilesExist(), is(false)); + FileSystem fileSystem = fileSystemFactory.create(); + File file = wayToObtainANonExistingFile.fileWithName(fileSystem, FILE_NAME); + byte[] dataToWrite = new byte[] {42, -43, 111, 104, -3, 83, -99, 30}; + + try (WritableFile writable = file.openWritable()) { + writable.write(ByteBuffer.wrap(dataToWrite)); + } + + assertThat(file, hasContent(dataToWrite)); + } + + @Theory + public void testWriteToExistingFileOverwritesContent(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAnExistingFile) { + assumeThat(wayToObtainAnExistingFile.returnedFilesExist(), is(true)); + FileSystem fileSystem = fileSystemFactory.create(); + byte[] originalData = new byte[] {32, 44, 1, -3, 4, 66, 4}; + File file = wayToObtainAnExistingFile.fileWithNameAndContent(fileSystem, FILE_NAME, originalData); + byte[] dataToWrite = new byte[] {42, -43, 111, 104, -3, 83, -99, 30}; + + try (WritableFile writable = file.openWritable()) { + writable.write(ByteBuffer.wrap(dataToWrite)); + } + + assertThat(file, hasContent(dataToWrite)); + } + + @Theory + public void testPartialWriteAtStartOfExistingFileOverwritesOnlyPartOfContents(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAnExistingFile) { + assumeThat(wayToObtainAnExistingFile.returnedFilesExist(), is(true)); + + // TODO implement partial writes in CryptoFileSystem + assumeThat(fileSystemFactory.toString(), not(containsString("Crypto"))); + + FileSystem fileSystem = fileSystemFactory.create(); + byte[] originalData = new byte[] {32, 44, 1, -3, 4, 66, 4}; + byte[] dataToWrite = new byte[] {1, 2, 3, 4}; + byte[] expectedData = new byte[] {1, 2, 3, 4, 4, 66, 4}; + File file = wayToObtainAnExistingFile.fileWithNameAndContent(fileSystem, FILE_NAME, originalData); + + try (WritableFile writable = file.openWritable()) { + writable.write(ByteBuffer.wrap(dataToWrite)); + } + + assertThat(file, hasContent(expectedData)); + } + + @Theory + public void testPartialWriteInTheMiddleOfExistingFileOverwritesOnlyPartOfContents(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAnExistingFile) { + assumeThat(wayToObtainAnExistingFile.returnedFilesExist(), is(true)); + + // TODO implement partial writes in CryptoFileSystem + assumeThat(fileSystemFactory.toString(), not(containsString("Crypto"))); + + FileSystem fileSystem = fileSystemFactory.create(); + byte[] originalData = new byte[] {32, 44, 1, -3, 4, 66, 4}; + byte[] dataToWrite = new byte[] {3, 4, 5, 6}; + byte[] expectedData = new byte[] {32, 44, 3, 4, 5, 6, 4}; + File file = wayToObtainAnExistingFile.fileWithNameAndContent(fileSystem, FILE_NAME, originalData); + + try (WritableFile writable = file.openWritable()) { + writable.position(2); + writable.write(ByteBuffer.wrap(dataToWrite)); + } + + assertThat(file, hasContent(expectedData)); + } + + @Theory + public void testPartialWriteAtEndOfExistingFileOverwritesOnlyPartOfContents(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAnExistingFile) { + assumeThat(wayToObtainAnExistingFile.returnedFilesExist(), is(true)); + + // TODO implement partial writes in CryptoFileSystem + assumeThat(fileSystemFactory.toString(), not(containsString("Crypto"))); + + FileSystem fileSystem = fileSystemFactory.create(); + byte[] originalData = new byte[] {-1, 44, 1, -3, 4, 66, 4}; + byte[] dataToWrite = new byte[] {4, 5, 6, 7}; + byte[] expectedData = new byte[] {-1, 44, 1, 4, 5, 6, 7}; + File file = wayToObtainAnExistingFile.fileWithNameAndContent(fileSystem, FILE_NAME, originalData); + + try (WritableFile writable = file.openWritable()) { + writable.position(3); + writable.write(ByteBuffer.wrap(dataToWrite)); + } + + assertThat(file, hasContent(expectedData)); + } + +} diff --git a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java index 00372013b..a8f409ce2 100644 --- a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java +++ b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java @@ -18,6 +18,7 @@ import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate; import org.cryptomator.filesystem.inmem.InMemoryFileSystem; import org.cryptomator.filesystem.invariants.FileSystemFactories.FileSystemFactory; import org.cryptomator.filesystem.nio.NioFileSystem; +import org.cryptomator.filesystem.shortening.ShorteningFileSystem; import org.mockito.Mockito; class FileSystemFactories implements Iterable { @@ -36,6 +37,8 @@ class FileSystemFactories implements Iterable { add("InMemoryFileSystem", this::createInMemoryFileSystem); add("CryptoFileSystem(NioFileSystem)", this::createCryptoFileSystemNio); add("CryptoFileSystem(InMemoryFileSystem)", this::createCryptoFileSystemInMemory); + add("ShorteningFileSystem(NioFileSystem)", this::createShorteningFileSystemNio); + add("ShorteningFileSystem(InMemoryFileSystem)", this::createShorteningFileSystemInMemory); } private FileSystem createNioFileSystem() { @@ -58,6 +61,16 @@ class FileSystemFactories implements Iterable { return new CryptoFileSystem(createNioFileSystem(), createCryptor(), Mockito.mock(CryptoFileSystemDelegate.class), "aPassphrase"); } + private FileSystem createShorteningFileSystemNio() { + FileSystem delegate = createNioFileSystem(); + return new ShorteningFileSystem(delegate.folder("d"), delegate.folder("m"), 3); + } + + private FileSystem createShorteningFileSystemInMemory() { + FileSystem delegate = createInMemoryFileSystem(); + return new ShorteningFileSystem(delegate.folder("d"), delegate.folder("m"), 3); + } + private Cryptor createCryptor() { Cryptor cryptor = new CryptorImpl(RANDOM_MOCK); cryptor.randomizeMasterkey(); diff --git a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileTests.java b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileTests.java index ac8afdfa1..0aaaaee16 100644 --- a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileTests.java +++ b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileTests.java @@ -1,15 +1,17 @@ package org.cryptomator.filesystem.invariants; -import static org.cryptomator.filesystem.invariants.matchers.NodeMatchers.hasContent; +import static org.cryptomator.common.test.matcher.OptionalMatcher.presentOptionalWithValueThat; +import static org.cryptomator.filesystem.invariants.matchers.InstantMatcher.inRangeInclusiveWithTolerance; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeThat; -import java.nio.ByteBuffer; +import java.io.UncheckedIOException; +import java.time.Instant; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.FileSystem; -import org.cryptomator.filesystem.WritableFile; +import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.invariants.FileSystemFactories.FileSystemFactory; import org.cryptomator.filesystem.invariants.WaysToObtainAFile.WayToObtainAFile; import org.cryptomator.filesystem.invariants.WaysToObtainAFolder.WayToObtainAFolder; @@ -34,21 +36,126 @@ public class FileTests { private static final String FILE_NAME = "fileName"; + private static final String FOLDER_NAME = "folderName"; + @Rule public final ExpectedException thrown = ExpectedException.none(); @Theory - public void testWriteToNonExistingFileCreatesFileWithContent(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainANonExistingFile) { + public void testNonExistingFileDoesNotExist(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainANonExistingFile) { assumeThat(wayToObtainANonExistingFile.returnedFilesExist(), is(false)); FileSystem fileSystem = fileSystemFactory.create(); File file = wayToObtainANonExistingFile.fileWithName(fileSystem, FILE_NAME); - byte[] dataToWrite = new byte[] {42, -43, 111, 104, -3, 83, -99, 30}; - try (WritableFile writable = file.openWritable()) { - writable.write(ByteBuffer.wrap(dataToWrite)); - } + assertThat(file.exists(), is(false)); + } - assertThat(file, hasContent(dataToWrite)); + @Theory + public void testExistingFileExist(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAnExistingFile) { + assumeThat(wayToObtainAnExistingFile.returnedFilesExist(), is(true)); + FileSystem fileSystem = fileSystemFactory.create(); + File file = wayToObtainAnExistingFile.fileWithName(fileSystem, FILE_NAME); + + assertThat(file.exists(), is(true)); + } + + @Theory + public void testNameOfFileIsFileName(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAFile) { + FileSystem fileSystem = fileSystemFactory.create(); + File file = wayToObtainAFile.fileWithName(fileSystem, FILE_NAME); + + assertThat(file.name(), is(FILE_NAME)); + } + + @Theory + public void testDeletedFileDoesNotExist(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAFile) { + FileSystem fileSystem = fileSystemFactory.create(); + File file = wayToObtainAFile.fileWithName(fileSystem, FILE_NAME); + file.delete(); + + assertThat(file.exists(), is(false)); + } + + @Theory + public void testParentOfFileInFilesystemIsFilesystem(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAFile) { + FileSystem fileSystem = fileSystemFactory.create(); + File file = wayToObtainAFile.fileWithName(fileSystem, FILE_NAME); + + assertThat(file.parent(), is(presentOptionalWithValueThat(is(fileSystem)))); + } + + @Theory + public void testParentOfFileInFolderIsFolder(FileSystemFactory fileSystemFactory, WayToObtainAFolder wayToObtainAFolder, WayToObtainAFile wayToObtainAFile) { + FileSystem fileSystem = fileSystemFactory.create(); + Folder folder = wayToObtainAFolder.folderWithName(fileSystem, FOLDER_NAME); + File file = wayToObtainAFile.fileWithName(folder, FILE_NAME); + + assertThat(file.parent(), is(presentOptionalWithValueThat(is(folder)))); + } + + @Theory + public void testFilesystemOfFileInFilesystemInFilesystem(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAFile) { + FileSystem fileSystem = fileSystemFactory.create(); + File file = wayToObtainAFile.fileWithName(fileSystem, FILE_NAME); + + assertThat(file.fileSystem(), is(fileSystem)); + } + + @Theory + public void testFilesystemOfFileInFolderIsFilesystem(FileSystemFactory fileSystemFactory, WayToObtainAFolder wayToObtainAFolder, WayToObtainAFile wayToObtainAFile) { + FileSystem fileSystem = fileSystemFactory.create(); + Folder folder = wayToObtainAFolder.folderWithName(fileSystem, FOLDER_NAME); + File file = wayToObtainAFile.fileWithName(folder, FILE_NAME); + + assertThat(file.fileSystem(), is(fileSystem)); + } + + @Theory + public void testFilesFromTwoFileSystemsDoNotBelongToSameFilesystem(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAFile) { + File file = wayToObtainAFile.fileWithName(fileSystemFactory.create(), FILE_NAME); + File otherFile = wayToObtainAFile.fileWithName(fileSystemFactory.create(), FILE_NAME); + + assertThat(file.belongsToSameFilesystem(otherFile), is(false)); + } + + @Theory + public void testFilesFromSameFileSystemsDoBelongToSameFilesystem(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAFile) { + FileSystem fileSystem = fileSystemFactory.create(); + File file = wayToObtainAFile.fileWithName(fileSystem, FILE_NAME); + File otherFile = wayToObtainAFile.fileWithName(fileSystem, FILE_NAME); + + assertThat(file.belongsToSameFilesystem(otherFile), is(true)); + } + + @Theory + public void testFilesBelongToSameFilesystemAsItself(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAFile) { + FileSystem fileSystem = fileSystemFactory.create(); + File file = wayToObtainAFile.fileWithName(fileSystem, FILE_NAME); + + assertThat(file.belongsToSameFilesystem(file), is(true)); + } + + @Theory + public void testLastModifiedIsInCorrectSecondsRange(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAnExistingFile) { + assumeThat(wayToObtainAnExistingFile.returnedFilesExist(), is(true)); + + FileSystem fileSystem = fileSystemFactory.create(); + Instant min = Instant.now(); + File file = wayToObtainAnExistingFile.fileWithName(fileSystem, FILE_NAME); + Instant max = Instant.now(); + + assertThat(file.lastModified(), is(inRangeInclusiveWithTolerance(min, max, 2000))); + } + + @Theory + public void testLastModifiedThrowsUncheckedIoExceptionForNonExistingFile(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainANonExistingFile) { + assumeThat(wayToObtainANonExistingFile.returnedFilesExist(), is(false)); + + FileSystem fileSystem = fileSystemFactory.create(); + + thrown.expect(UncheckedIOException.class); + + System.out.println(wayToObtainANonExistingFile.fileWithName(fileSystem, FILE_NAME).lastModified()); } } diff --git a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/WaysToObtainAFile.java b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/WaysToObtainAFile.java index 0d34c4f34..204f3fde7 100644 --- a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/WaysToObtainAFile.java +++ b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/WaysToObtainAFile.java @@ -16,19 +16,60 @@ class WaysToObtainAFile implements Iterable { public WaysToObtainAFile() { addNonExisting("invoke file", this::invokeFile); + addNonExisting("delete file created by writing to it", this::deleteFileCreatedByWritingToIt); addExisting("create file by writing to it", this::createFileByWritingToIt); + addExisting("create file by copying", this::createFileByCopying); + addExisting("create file by moving", this::createFileByMoving); } private File invokeFile(Folder parent, String name, byte[] content) { return parent.file(name); } - private File createFileByWritingToIt(Folder parent, String name, byte[] content) { + private File deleteFileCreatedByWritingToIt(Folder parent, String name, byte[] content) { + boolean deleteParent = !parent.exists(); + parent.create(); File result = parent.file(name); try (WritableFile writable = result.openWritable()) { writable.write(ByteBuffer.wrap(content)); } + result.delete(); + if (deleteParent) { + parent.delete(); + } + return result; + } + + private File createFileByWritingToIt(Folder parent, String name, byte[] content) { + parent.create(); + File result = parent.file(name); + try (WritableFile writable = result.openWritable()) { + writable.write(ByteBuffer.wrap(content)); + } + return result; + } + + private File createFileByCopying(Folder parent, String name, byte[] content) { + parent.create(); + File tmp = parent.file(name + ".createFileByCopying.tmp"); + try (WritableFile writable = tmp.openWritable()) { + writable.write(ByteBuffer.wrap(content)); + } + File result = parent.file(name); + tmp.copyTo(result); + tmp.delete(); + return result; + } + + private File createFileByMoving(Folder parent, String name, byte[] content) { + parent.create(); + File tmp = parent.file(name + ".createFileByCopying.tmp"); + try (WritableFile writable = tmp.openWritable()) { + writable.write(ByteBuffer.wrap(content)); + } + File result = parent.file(name); + tmp.moveTo(result); return result; } diff --git a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/matchers/InstantMatcher.java b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/matchers/InstantMatcher.java new file mode 100644 index 000000000..3f13ae51a --- /dev/null +++ b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/matchers/InstantMatcher.java @@ -0,0 +1,35 @@ +package org.cryptomator.filesystem.invariants.matchers; + +import static java.lang.String.format; + +import java.time.Instant; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +public class InstantMatcher { + + public static Matcher inRangeInclusiveWithTolerance(Instant min, Instant max, int toleranceInMilliseconds) { + return inRangeInclusive(min.minusMillis(toleranceInMilliseconds), max.plusMillis(toleranceInMilliseconds)); + } + + public static Matcher inRangeInclusive(Instant min, Instant max) { + return new TypeSafeDiagnosingMatcher(Instant.class) { + @Override + public void describeTo(Description description) { + description.appendText(format("an Instant in [%s,%s]", min, max)); + } + + @Override + protected boolean matchesSafely(Instant item, Description mismatchDescription) { + if (item.isBefore(min) || item.isAfter(max)) { + mismatchDescription.appendText("the instant ").appendValue(item); + return false; + } + return true; + } + }; + } + +}