diff --git a/main/commons-test/pom.xml b/main/commons-test/pom.xml index bba69fa84..7b7d80e4c 100644 --- a/main/commons-test/pom.xml +++ b/main/commons-test/pom.xml @@ -31,6 +31,11 @@ org.hamcrest hamcrest-all + + + org.cryptomator + commons + diff --git a/main/commons/pom.xml b/main/commons/pom.xml index 08fb094ed..805f16e0a 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -12,11 +12,24 @@ commons Cryptomator common Shared utilities - + - org.cryptomator - commons-test + junit + junit + + + de.bechte.junit + junit-hierarchicalcontextrunner + + + org.hamcrest + hamcrest-all + compile + + + org.hamcrest + hamcrest-all diff --git a/main/commons/src/main/java/org/cryptomator/common/CachingSupplier.java b/main/commons/src/main/java/org/cryptomator/common/CachingSupplier.java new file mode 100644 index 000000000..717d63920 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/CachingSupplier.java @@ -0,0 +1,26 @@ +package org.cryptomator.common; + +import java.util.function.Supplier; + +public class CachingSupplier implements Supplier { + + public static Supplier from(Supplier delegate) { + return new CachingSupplier<>(delegate); + } + + private Supplier delegate; + + private CachingSupplier(Supplier delegate) { + this.delegate = () -> { + T result = delegate.get(); + CachingSupplier.this.delegate = () -> result; + return result; + }; + } + + @Override + public T get() { + return delegate.get(); + } + +} diff --git a/main/commons/src/test/java/org/cryptomator/common/CachingSupplierTest.java b/main/commons/src/test/java/org/cryptomator/common/CachingSupplierTest.java new file mode 100644 index 000000000..4917c5eac --- /dev/null +++ b/main/commons/src/test/java/org/cryptomator/common/CachingSupplierTest.java @@ -0,0 +1,43 @@ +package org.cryptomator.common; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.function.Supplier; + +import org.junit.Test; + +public class CachingSupplierTest { + + @Test + public void testInvokingGetInvokesDelegate() { + @SuppressWarnings("unchecked") + Supplier delegate = mock(Supplier.class); + Object expectedResult = new Object(); + when(delegate.get()).thenReturn(expectedResult); + Supplier inTest = CachingSupplier.from(delegate); + + Object result = inTest.get(); + + assertThat(result, is(expectedResult)); + } + + @Test + public void testInvokingGetTwiceDoesNotInvokeDelegateTwice() { + @SuppressWarnings("unchecked") + Supplier delegate = mock(Supplier.class); + Object expectedResult = new Object(); + when(delegate.get()).thenReturn(expectedResult); + Supplier inTest = CachingSupplier.from(delegate); + + inTest.get(); + Object result = inTest.get(); + + assertThat(result, is(expectedResult)); + verify(delegate).get(); + } + +} diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystem.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystem.java index 563b1f068..b96ed1b9f 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystem.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystem.java @@ -23,4 +23,8 @@ public interface FileSystem extends Folder { return Optional.empty(); } + default boolean supports(FileSystemFeature feature) { + return false; + } + } diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystemFeature.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystemFeature.java new file mode 100644 index 000000000..af683d6c6 --- /dev/null +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystemFeature.java @@ -0,0 +1,5 @@ +package org.cryptomator.filesystem; + +public enum FileSystemFeature { + CREATION_TIME_FEATURE +} diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java index 89c40ff6e..8d11d5a17 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java @@ -7,6 +7,7 @@ package org.cryptomator.filesystem; import java.io.IOException; import java.io.UncheckedIOException; +import java.time.Instant; import java.util.stream.Stream; /** @@ -148,4 +149,18 @@ public interface Folder extends Node { } } + /** + * + * Sets the creation time of the folder. + * + * Setting the creation time may not be supported by all {@link FileSystem FileSystems}. If the {@code FileSystem} this {@code Folder} belongs to does not support the + * {@link FileSystemFeature#CREATION_TIME_FEATURE} the behavior of this method is unspecified. + * + * @param instant the time to set as creation time + * @see FileSystem#supports(Class) + */ + default void setCreationTime(Instant instant) throws UncheckedIOException { + throw new UncheckedIOException(new IOException("CreationTime not supported")); + } + } diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java index 8a8cc81fe..841eba8b2 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java @@ -5,6 +5,7 @@ ******************************************************************************/ package org.cryptomator.filesystem; +import java.io.IOException; import java.io.UncheckedIOException; import java.time.Instant; import java.util.Optional; @@ -33,6 +34,20 @@ public interface Node { Instant lastModified() throws UncheckedIOException; + /** + * + * Determines the creation time of this node. + * + * Setting the creation time may not be supported by all {@link FileSystem FileSystems}. If the {@code FileSystem} this {@code Node} belongs to does not support the + * {@link FileSystemFeature#CREATION_TIME_FEATURE} the behavior of this method is unspecified. + * + * @returns the creation time of the file. + * @see FileSystem#supports(Class) + */ + default Instant creationTime() throws UncheckedIOException { + throw new UncheckedIOException(new IOException("CreationTime not supported")); + } + /** * @return the {@link FileSystem} this Node belongs to */ 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 9e44eb8a8..e14e6987f 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 @@ -24,6 +24,20 @@ public interface WritableFile extends WritableByteChannel { void setLastModified(Instant instant) throws UncheckedIOException; + /** + * + * Sets the creation time of the file. + * + * Setting the creation time may not be supported by all {@link FileSystem FileSystems}. If the {@code FileSystem} this {@code WritableFile} belongs to does not support the + * {@link FileSystemFeature#CREATION_TIME_FEATURE} the behavior of this method is unspecified. + * + * @param instant the time to set as creation time + * @see FileSystem#supports(Class) + */ + default void setCreationTime(Instant instant) throws UncheckedIOException { + throw new UncheckedIOException(new IOException("CreationTime not supported")); + } + /** * * Deletes this file from the file system. diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java index 960bf5389..de763a208 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java @@ -9,6 +9,7 @@ package org.cryptomator.filesystem.delegating; import java.io.UncheckedIOException; +import java.time.Instant; import java.util.Optional; import java.util.stream.Stream; @@ -90,4 +91,9 @@ public abstract class DelegatingFolder implements Node { return delegate.lastModified(); } + @Override + public Instant creationTime() throws UncheckedIOException { + return delegate.creationTime(); + } + @Override public int hashCode() { return delegate.hashCode(); diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingWritableFile.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingWritableFile.java index 905153b7a..66bd05712 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingWritableFile.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingWritableFile.java @@ -67,4 +67,9 @@ public class DelegatingWritableFile implements WritableFile { delegate.close(); } + @Override + public void setCreationTime(Instant instant) throws UncheckedIOException { + delegate.setCreationTime(instant); + } + } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFileSystem.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFileSystem.java index 9d182b3bb..636c3e216 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFileSystem.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFileSystem.java @@ -9,6 +9,7 @@ package org.cryptomator.filesystem.blockaligned; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; class BlockAlignedFileSystem extends BlockAlignedFolder implements FileSystem { @@ -17,4 +18,9 @@ class BlockAlignedFileSystem extends BlockAlignedFolder implements FileSystem { super(null, delegate, blockSize); } + @Override + public boolean supports(FileSystemFeature feature) { + return delegate.fileSystem().supports(feature); + } + } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java index 9c60ffeb8..8c3402418 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java @@ -54,4 +54,9 @@ public class CryptoFile extends CryptoNode implements File { return toString().compareTo(o.toString()); } + @Override + public Instant creationTime() throws UncheckedIOException { + return physicalFile().creationTime(); + } + } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java index b19555e20..f4bccb75a 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java @@ -15,6 +15,7 @@ import org.cryptomator.crypto.engine.Cryptor; import org.cryptomator.crypto.engine.InvalidPassphraseException; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; @@ -104,6 +105,11 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem { physicalFolder().create(); } + @Override + public boolean supports(FileSystemFeature feature) { + return physicalRoot.fileSystem().supports(feature); + } + @Override public String toString() { return physicalRoot + ":::/"; diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java index a07105b30..bd9902e77 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java @@ -146,6 +146,11 @@ class CryptoFolder extends CryptoNode implements Folder { } + @Override + public Instant creationTime() throws UncheckedIOException { + return physicalFile().creationTime(); + } + @Override public String toString() { return parent.toString() + name + "/"; diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java index bd2bc5faa..443bd6737 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java @@ -93,6 +93,11 @@ class CryptoWritableFile implements WritableFile { throw new UnsupportedOperationException("Truncate not supported yet"); } + @Override + public void setCreationTime(Instant instant) throws UncheckedIOException { + file.setCreationTime(instant); + } + @Override public boolean isOpen() { return file.isOpen(); diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java index b3ce61b1f..24c7dbddd 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java @@ -26,8 +26,8 @@ class InMemoryFile extends InMemoryNode implements File { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private ByteBuffer content = ByteBuffer.allocate(0); - public InMemoryFile(InMemoryFolder parent, String name, Instant lastModified) { - super(parent, name, lastModified); + public InMemoryFile(InMemoryFolder parent, String name, Instant lastModified, Instant creationTime) { + super(parent, name, lastModified, creationTime); } @Override @@ -48,19 +48,24 @@ class InMemoryFile extends InMemoryNode implements File { parent.children.compute(this.name(), (k, v) -> { if (v == null || v == this) { this.lastModified = Instant.now(); + this.creationTime = Instant.now(); return this; } else { throw new UncheckedIOException(new FileExistsException(k)); } }); parent.volatileFiles.remove(name); - return new InMemoryWritableFile(this::setLastModified, this::getContent, this::setContent, this::delete, writeLock); + return new InMemoryWritableFile(this::setLastModified, this::setCreationTime, this::getContent, this::setContent, this::delete, writeLock); } private void setLastModified(Instant lastModified) { this.lastModified = lastModified; } + private void setCreationTime(Instant creationTime) { + this.creationTime = creationTime; + } + private ByteBuffer getContent() { return content; } @@ -75,7 +80,7 @@ class InMemoryFile extends InMemoryNode implements File { // returning null removes the entry. return null; }); - assert!this.exists(); + assert !this.exists(); } @Override diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java index bc1b8ce3f..8b4fc35c7 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java @@ -12,11 +12,12 @@ import java.time.Instant; import java.util.Optional; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; public class InMemoryFileSystem extends InMemoryFolder implements FileSystem { public InMemoryFileSystem() { - super(null, "", Instant.now()); + super(null, "", Instant.now(), Instant.now()); } @Override @@ -39,4 +40,9 @@ public class InMemoryFileSystem extends InMemoryFolder implements FileSystem { return "/"; } + @Override + public boolean supports(FileSystemFeature feature) { + return feature == FileSystemFeature.CREATION_TIME_FEATURE; + } + } diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java index 509fdd77e..fc3d9b979 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java @@ -25,8 +25,8 @@ class InMemoryFolder extends InMemoryNode implements Folder { final Map volatileFiles = new HashMap<>(); final Map volatileFolders = new HashMap<>(); - public InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified) { - super(parent, name, lastModified); + public InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified, Instant creationTime) { + super(parent, name, lastModified, creationTime); } @Override @@ -41,7 +41,7 @@ class InMemoryFolder extends InMemoryNode implements Folder { return (InMemoryFile) node; } else { return volatileFiles.computeIfAbsent(name, (n) -> { - return new InMemoryFile(this, n, Instant.MIN); + return new InMemoryFile(this, n, Instant.MIN, Instant.MIN); }); } } @@ -53,7 +53,7 @@ class InMemoryFolder extends InMemoryNode implements Folder { return (InMemoryFolder) node; } else { return volatileFolders.computeIfAbsent(name, (n) -> { - return new InMemoryFolder(this, n, Instant.MIN); + return new InMemoryFolder(this, n, Instant.MIN, Instant.MIN); }); } } @@ -74,6 +74,7 @@ class InMemoryFolder extends InMemoryNode implements Folder { }); parent.volatileFolders.remove(name); assert this.exists(); + creationTime = Instant.now(); } @Override @@ -81,11 +82,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 @@ -107,7 +108,7 @@ class InMemoryFolder extends InMemoryNode implements Folder { subFolder.delete(); } } - assert!this.exists(); + assert !this.exists(); } @Override 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 fbe899529..ea106b472 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 @@ -8,6 +8,8 @@ *******************************************************************************/ package org.cryptomator.filesystem.inmem; +import java.io.IOException; +import java.io.UncheckedIOException; import java.time.Instant; import java.util.Optional; @@ -18,11 +20,13 @@ class InMemoryNode implements Node { protected final InMemoryFolder parent; protected final String name; protected Instant lastModified; + protected Instant creationTime; - public InMemoryNode(InMemoryFolder parent, String name, Instant lastModified) { + public InMemoryNode(InMemoryFolder parent, String name, Instant lastModified, Instant creationTime) { this.parent = parent; this.name = name; this.lastModified = lastModified; + this.creationTime = creationTime; } @Override @@ -66,4 +70,13 @@ class InMemoryNode implements Node { } } + @Override + public Instant creationTime() throws UncheckedIOException { + if (exists()) { + return creationTime; + } else { + throw new UncheckedIOException(new IOException("Node does not exist")); + } + } + } diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryWritableFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryWritableFile.java index d12a83412..f621f6696 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryWritableFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryWritableFile.java @@ -21,6 +21,7 @@ import org.cryptomator.io.ByteBuffers; public class InMemoryWritableFile implements WritableFile { private final Consumer lastModifiedSetter; + private final Consumer creationTimeSetter; private final Supplier contentGetter; private final Consumer contentSetter; private final Consumer deleter; @@ -29,12 +30,14 @@ public class InMemoryWritableFile implements WritableFile { private boolean open; private int position = 0; - public InMemoryWritableFile(Consumer lastModifiedSetter, Supplier contentGetter, Consumer contentSetter, Consumer deleter, WriteLock writeLock) { + public InMemoryWritableFile(Consumer lastModifiedSetter, Consumer creationTimeSetter, Supplier contentGetter, Consumer contentSetter, Consumer deleter, + WriteLock writeLock) { this.lastModifiedSetter = lastModifiedSetter; this.contentGetter = contentGetter; this.contentSetter = contentSetter; this.deleter = deleter; this.writeLock = writeLock; + this.creationTimeSetter = creationTimeSetter; } @Override @@ -98,4 +101,9 @@ public class InMemoryWritableFile implements WritableFile { lastModifiedSetter.accept(Instant.now()); } + @Override + public void setCreationTime(Instant instant) throws UncheckedIOException { + creationTimeSetter.accept(instant); + } + } diff --git a/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileTest.java b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileTest.java new file mode 100644 index 000000000..8f19f811a --- /dev/null +++ b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileTest.java @@ -0,0 +1,56 @@ +package org.cryptomator.filesystem.inmem; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.io.UncheckedIOException; +import java.time.Instant; + +import org.cryptomator.filesystem.WritableFile; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class InMemoryFileTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testCreationTimeOfNonExistingFileThrowsUncheckedIOException() { + InMemoryFileSystem fileSystem = new InMemoryFileSystem(); + InMemoryFile inTest = fileSystem.file("foo"); + + thrown.expect(UncheckedIOException.class); + + inTest.creationTime(); + } + + @Test + public void testCreationTimeOfCreatedFileIsSetToInstantDuringCreation() { + InMemoryFileSystem fileSystem = new InMemoryFileSystem(); + InMemoryFile inTest = fileSystem.file("foo"); + + Instant minCreationTime = Instant.now(); + Instant maxCreationTime; + try (WritableFile writable = inTest.openWritable()) { + maxCreationTime = Instant.now(); + } + + assertThat(inTest.creationTime().isBefore(minCreationTime), is(false)); + assertThat(inTest.creationTime().isAfter(maxCreationTime), is(false)); + } + + @Test + public void testCreationTimeSetInWritableFileIsSaved() { + Instant creationTime = Instant.parse("2015-03-23T21:11:32Z"); + InMemoryFileSystem fileSystem = new InMemoryFileSystem(); + InMemoryFile inTest = fileSystem.file("foo"); + try (WritableFile writable = inTest.openWritable()) { + writable.setCreationTime(creationTime); + } + + assertThat(inTest.creationTime(), is(creationTime)); + } + +} diff --git a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFileSystem.java b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFileSystem.java index 23786805a..74bf3ca54 100644 --- a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFileSystem.java +++ b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFileSystem.java @@ -3,6 +3,7 @@ package org.cryptomator.filesystem.blacklisting; import java.util.function.Predicate; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.Node; @@ -12,4 +13,9 @@ class BlacklistingFileSystem extends BlacklistingFolder implements FileSystem { super(null, root, hiddenNodes); } + @Override + public boolean supports(FileSystemFeature feature) { + return delegate.fileSystem().supports(feature); + } + } diff --git a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFileSystem.java b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFileSystem.java index 0a5f8f1b3..379aced28 100644 --- a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFileSystem.java +++ b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFileSystem.java @@ -1,6 +1,7 @@ package org.cryptomator.filesystem.shortening; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; class ShorteningFileSystem extends ShorteningFolder implements FileSystem { @@ -9,4 +10,9 @@ class ShorteningFileSystem extends ShorteningFolder implements FileSystem { super(null, root, "", new FilenameShortener(metadataRoot, threshold)); } + @Override + public boolean supports(FileSystemFeature feature) { + return delegate.fileSystem().supports(feature); + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java index 9e1518e27..b57b07ece 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java @@ -8,12 +8,19 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.OpenOption; import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileTime; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + class DefaultNioAccess implements NioAccess { + private static final Logger LOG = LoggerFactory.getLogger(DefaultNioAccess.class); + @Override public FileChannel open(Path path, OpenOption... options) throws IOException { return FileChannel.open(path, options); @@ -74,4 +81,30 @@ class DefaultNioAccess implements NioAccess { return FileSystems.getDefault().getSeparator(); } + @Override + public FileTime getCreationTime(Path path, LinkOption... options) throws IOException { + return Files.readAttributes(path, BasicFileAttributes.class, options).creationTime(); + } + + @Override + public void setCreationTime(Path path, FileTime creationTime, LinkOption... options) throws IOException { + Files.getFileAttributeView(path, BasicFileAttributeView.class, options).setTimes(null, null, creationTime); + } + + @Override + public boolean supportsCreationTime(Path path) { + try { + Path file = Files.createTempFile(path, "creationTimeCheck", "tmp"); + long expected = 1184725140000L; + long millisecondsInADay = 86400000L; + FileTime fileTime = FileTime.fromMillis(expected); + Files.getFileAttributeView(file, BasicFileAttributeView.class).setTimes(null, null, fileTime); + long actual = Files.readAttributes(file, BasicFileAttributes.class).creationTime().toMillis(); + return Math.abs(expected - actual) <= millisecondsInADay; + } catch (IOException e) { + LOG.info("supportsCreationTime failed", e); + return false; + } + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java index e8cb7004f..1d98513e5 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java @@ -40,4 +40,10 @@ interface NioAccess { String separator(); + FileTime getCreationTime(Path path, LinkOption... options) throws IOException; + + void setCreationTime(Path path, FileTime creationTime, LinkOption... options) throws IOException; + + boolean supportsCreationTime(Path path); + } 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 195e6e0bf..1d7fa0927 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 @@ -82,4 +82,12 @@ class NioFile extends NioNode implements File { return format("NioFile(%s)", path); } + @Override + public Instant creationTime() throws UncheckedIOException { + if (nioAccess.exists(path) && !exists()) { + throw new UncheckedIOException(new IOException(format("%s is a folder", path))); + } + return super.creationTime(); + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java index 827c75ec5..c2566abd0 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java @@ -2,11 +2,16 @@ package org.cryptomator.filesystem.nio; import java.nio.file.Path; import java.util.Optional; +import java.util.function.Supplier; +import org.cryptomator.common.CachingSupplier; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; public class NioFileSystem extends NioFolder implements FileSystem { + private final Supplier supportsCreationTime = CachingSupplier.from(this::supportsCreationTime); + public static NioFileSystem rootedAt(Path root) { return new NioFileSystem(root); } @@ -16,4 +21,17 @@ public class NioFileSystem extends NioFolder implements FileSystem { create(); } + @Override + public boolean supports(FileSystemFeature feature) { + if (feature == FileSystemFeature.CREATION_TIME_FEATURE) { + return supportsCreationTime.get(); + } else { + return false; + } + } + + private boolean supportsCreationTime() { + return nioAccess.supportsCreationTime(path); + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java index bd83bf17b..74587e235 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java @@ -111,6 +111,14 @@ class NioFolder extends NioNode implements Folder { return path; } + @Override + public Instant creationTime() throws UncheckedIOException { + if (nioAccess.exists(path) && !nioAccess.isDirectory(path)) { + throw new UncheckedIOException(new IOException(format("%s is a file", path))); + } + return super.creationTime(); + } + @Override public String toString() { return format("NioFolder(%s)", path); diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java index 9fb0a972a..ae6d2fedd 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java @@ -43,4 +43,13 @@ abstract class NioNode implements Node { } } + @Override + public Instant creationTime() throws UncheckedIOException { + try { + return nioAccess.getCreationTime(path).toInstant(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } 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 index c6670431c..9b1968b6b 100644 --- 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 @@ -128,6 +128,17 @@ class WritableNioFile implements WritableFile { channel.truncate(0); } + @Override + public void setCreationTime(Instant instant) throws UncheckedIOException { + assertOpen(); + ensureChannelIsOpened(); + try { + nioAccess.setCreationTime(path, FileTime.from(instant)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + @Override public void close() throws UncheckedIOException { if (!open) { diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/DefaultNioAccessTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/DefaultNioAccessTest.java index d2caa1b2b..0f50c012c 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/DefaultNioAccessTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/DefaultNioAccessTest.java @@ -16,6 +16,7 @@ import static org.mockito.Mockito.when; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; @@ -34,7 +35,12 @@ import java.util.HashSet; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import de.bechte.junit.runners.context.HierarchicalContextRunner; + +@RunWith(HierarchicalContextRunner.class) public class DefaultNioAccessTest { private DefaultNioAccess inTest = new DefaultNioAccess(); @@ -186,6 +192,159 @@ public class DefaultNioAccessTest { verify(implCloseChannel).run(); } + @Test + public void testGetCreationTimeReadsAttributesUsingProviderAndReturnsValueFromThem() throws IOException { + FileTime expectedValue = FileTime.from(Instant.parse("2016-01-08T22:32:00Z")); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(attributes.creationTime()).thenReturn(expectedValue); + LinkOption[] options = {LinkOption.NOFOLLOW_LINKS}; + when(provider.readAttributes(path, BasicFileAttributes.class, options)).thenReturn(attributes); + + FileTime result = inTest.getCreationTime(path, options); + + assertThat(result, is(expectedValue)); + } + + @Test + public void testSetCreationTimeGetsAttributeViewUsingProviderAndSetsCreationTimeUsingIt() throws IOException { + FileTime fileTime = FileTime.from(Instant.now()); + BasicFileAttributeView attributes = mock(BasicFileAttributeView.class); + LinkOption[] options = {LinkOption.NOFOLLOW_LINKS}; + when(provider.getFileAttributeView(path, BasicFileAttributeView.class, options)).thenReturn(attributes); + + inTest.setCreationTime(path, fileTime, options); + + verify(attributes).setTimes(null, null, fileTime); + } + + public class SupportsCreationTimeTests { + + @Test + public void testSupportsCreationTimeReturnsTrueIfGetCreationTimeIsADayOfFromSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet + millisecondsInADay)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(true)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeReturnsTrueIfGetCreationTimeIsADayOfBeforeSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet - millisecondsInADay)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(true)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeSucceedsIfGetCreationTimeIsADayOfBeforeSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet - millisecondsInADay)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(true)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeReturnsFalseIfGetCreationTimeIsMoreAsADayOfFromSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet + millisecondsInADay + 1)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(false)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeReturnsFalseIfGetCreationTimeIsMoreAsADayBeforeSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet - millisecondsInADay - 1)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(false)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeReturnsFalseIfIOExceptionOccurs() throws IOException { + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenThrow(new IOException()); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(false)); + } + + } + @Test public void testSeparatorReturnsSeparatorOfDefaultFileSystem() { assertThat(inTest.separator(), is(FileSystems.getDefault().getSeparator())); diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java index 07ef1eb66..916797ba2 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Optional; +import org.cryptomator.filesystem.FileSystemFeature; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -51,6 +52,31 @@ public class NioFileSystemTest { verify(nioAccess).createDirectories(path); } + @Test + public void testSupportsCreationTimeDelegatesToNioAccessWithTrue() { + when(nioAccess.supportsCreationTime(path)).thenReturn(true); + + boolean result = inTest.supports(FileSystemFeature.CREATION_TIME_FEATURE); + + assertThat(result, is(true)); + } + + @Test + public void testSupportsWithOtherFeatureReturnsFalse() { + boolean result = inTest.supports(null); + + assertThat(result, is(false)); + } + + @Test + public void testSupportsCreationTimeDelegatesToNioAccessWithFalse() { + when(nioAccess.supportsCreationTime(path)).thenReturn(false); + + boolean result = inTest.supports(FileSystemFeature.CREATION_TIME_FEATURE); + + assertThat(result, is(false)); + } + @After public void tearDown() { InstanceFactory.DEFAULT.reset(); 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 index 09460c007..3b0ee216c 100644 --- 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 @@ -295,6 +295,46 @@ public class NioFileTest { } + public class CreationTime { + + @Test + public void testCreationTimeDelegatesToNioAccessCreationTime() throws IOException { + Instant exectedResult = Instant.parse("2016-01-08T19:49:00Z"); + when(nioAccess.getCreationTime(path)).thenReturn(FileTime.from(exectedResult)); + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isRegularFile(path)).thenReturn(true); + + Instant result = inTest.creationTime(); + + assertThat(result, is(exectedResult)); + } + + @Test + public void testCreationTimeWrapsIOExceptionFromNioAccessCreationTimeInUncheckedIOException() throws IOException { + IOException exceptionFromCreationTime = new IOException(); + when(nioAccess.getCreationTime(path)).thenThrow(exceptionFromCreationTime); + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isRegularFile(path)).thenReturn(true); + + thrown.expect(UncheckedIOException.class); + thrown.expectCause(is(exceptionFromCreationTime)); + + inTest.creationTime(); + } + + @Test + public void testCreationTimeThrowsExceptionIfFileIsNoRegularFile() { + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isRegularFile(path)).thenReturn(false); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage(format("%s is a folder", path)); + + inTest.creationTime(); + } + + } + @Test public void testNameReturnsFileNameOfPath() { Path fileName = mock(Path.class); 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 8b74ff94e..e4404dcee 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 @@ -410,6 +410,57 @@ public class NioFolderTest { } + public class CreationTimeTests { + + @Test + public void testCreationTimeDelegatesToNioAccessCreationTimeForExistingFolder() throws IOException { + Instant exectedResult = Instant.parse("2016-01-08T19:49:00Z"); + when(nioAccess.getCreationTime(path)).thenReturn(FileTime.from(exectedResult)); + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isDirectory(path)).thenReturn(true); + + Instant result = inTest.creationTime(); + + assertThat(result, is(exectedResult)); + } + + @Test + public void testCreationTimeDelegatesToNioAccessCreationTimeForNonExistingFolder() throws IOException { + Instant exectedResult = Instant.parse("2016-01-08T19:49:00Z"); + when(nioAccess.getCreationTime(path)).thenReturn(FileTime.from(exectedResult)); + when(nioAccess.exists(path)).thenReturn(false); + + Instant result = inTest.creationTime(); + + assertThat(result, is(exectedResult)); + } + + @Test + public void testCreationTimeWrapsIOExceptionFromNioAccessCreationTimeInUncheckedIOException() throws IOException { + IOException exceptionFromCreationTime = new IOException(); + when(nioAccess.getCreationTime(path)).thenThrow(exceptionFromCreationTime); + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isDirectory(path)).thenReturn(true); + + thrown.expect(UncheckedIOException.class); + thrown.expectCause(is(exceptionFromCreationTime)); + + inTest.creationTime(); + } + + @Test + public void testCreationTimeThrowsExceptionIfFileIsNoDirectory() { + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isDirectory(path)).thenReturn(false); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage(format("%s is a file", path)); + + inTest.creationTime(); + } + + } + public class DeleteTests { @Test diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WritableNioFileTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WritableNioFileTest.java index 6d55e6b64..6027ef59b 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WritableNioFileTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WritableNioFileTest.java @@ -5,7 +5,9 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static org.cryptomator.filesystem.nio.OpenMode.WRITE; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.same; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -286,6 +288,44 @@ public class WritableNioFileTest { } + public class SetCreationTimeTests { + + @Test + public void testSetCreationTimeFailsIfNotOpen() { + Instant irrelevant = null; + inTest.close(); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage("already closed"); + + inTest.setCreationTime(irrelevant); + } + + @Test + public void testSetCreationTimeOpensChannelIfClosedAndInvokesNioAccessSetCreationTimeAfterwards() throws IOException { + Instant instant = Instant.parse("2016-01-08T22:32:00Z"); + + inTest.setCreationTime(instant); + + InOrder inOrder = inOrder(nioAccess, channel); + inOrder.verify(channel).openIfClosed(OpenMode.WRITE); + inOrder.verify(nioAccess).setCreationTime(path, FileTime.from(instant)); + } + + @Test + public void testSetCreationTimeWrapsIOExceptionFromSetCreationTimeInUncheckedIOException() throws IOException { + IOException exceptionFromSetCreationTime = new IOException(); + Instant irrelevant = Instant.now(); + doThrow(exceptionFromSetCreationTime).when(nioAccess).setCreationTime(same(path), any()); + + thrown.expect(UncheckedIOException.class); + thrown.expectCause(is(exceptionFromSetCreationTime)); + + inTest.setCreationTime(irrelevant); + } + + } + public class DeleteTests { @Test diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java index 7aa0a17d6..40facff16 100644 --- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java @@ -21,6 +21,7 @@ import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.io.InputContext; import org.apache.jackrabbit.webdav.io.OutputContext; import org.apache.jackrabbit.webdav.lock.LockManager; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; import org.cryptomator.filesystem.jackrabbit.FileLocator; @@ -98,8 +99,11 @@ class DavFile extends DavNode { @Override protected void setCreationTime(Instant instant) { - // TODO Auto-generated method stub - + if (node.fileSystem().supports(FileSystemFeature.CREATION_TIME_FEATURE)) { + try (WritableFile writable = node.openWritable()) { + writable.setCreationTime(instant); + } + } } } diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java index 98969a353..54c819bd5 100644 --- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java @@ -31,6 +31,7 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import org.apache.jackrabbit.webdav.property.ResourceType; import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.Node; import org.cryptomator.filesystem.WritableFile; @@ -154,8 +155,9 @@ class DavFolder extends DavNode { @Override protected void setCreationTime(Instant instant) { - // TODO Auto-generated method stub - + if (node.fileSystem().supports(FileSystemFeature.CREATION_TIME_FEATURE)) { + node.setCreationTime(instant); + } } } diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavNode.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavNode.java index 71be3e145..152358394 100644 --- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavNode.java +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavNode.java @@ -28,7 +28,9 @@ import org.apache.jackrabbit.webdav.property.DavProperty; import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import org.apache.jackrabbit.webdav.property.PropEntry; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.jackrabbit.FileSystemResourceLocator; abstract class DavNode implements DavResource { @@ -106,7 +108,16 @@ abstract class DavNode implements DavResour @Override public DavProperty> getProperty(DavPropertyName name) { - return getProperties().get(name); + final String namespacelessPropertyName = name.getName(); + if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName)) { + if (node.fileSystem().supports(FileSystemFeature.CREATION_TIME_FEATURE)) { + return new DefaultDavProperty<>(name, DateTimeFormatter.RFC_1123_DATE_TIME.format(node.creationTime())); + } else { + return null; + } + } else { + return getProperties().get(name); + } } @Override @@ -116,8 +127,6 @@ abstract class DavNode implements DavResour @Override public void setProperty(DavProperty> property) throws DavException { - getProperties().add(property); - final String namespacelessPropertyName = property.getName().getName(); if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) { final String createDateStr = (String) property.getValue(); @@ -127,6 +136,9 @@ abstract class DavNode implements DavResour final String lastModifiedTimeStr = (String) property.getValue(); final Instant modificationTime = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(lastModifiedTimeStr)); this.setModificationTime(modificationTime); + getProperties().add(property); + } else { + getProperties().add(property); } }
+ * Sets the creation time of the folder. + *
+ * Setting the creation time may not be supported by all {@link FileSystem FileSystems}. If the {@code FileSystem} this {@code Folder} belongs to does not support the + * {@link FileSystemFeature#CREATION_TIME_FEATURE} the behavior of this method is unspecified. + * + * @param instant the time to set as creation time + * @see FileSystem#supports(Class) + */ + default void setCreationTime(Instant instant) throws UncheckedIOException { + throw new UncheckedIOException(new IOException("CreationTime not supported")); + } + } diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java index 8a8cc81fe..841eba8b2 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Node.java @@ -5,6 +5,7 @@ ******************************************************************************/ package org.cryptomator.filesystem; +import java.io.IOException; import java.io.UncheckedIOException; import java.time.Instant; import java.util.Optional; @@ -33,6 +34,20 @@ public interface Node { Instant lastModified() throws UncheckedIOException; + /** + *
+ * Determines the creation time of this node. + *
+ * Setting the creation time may not be supported by all {@link FileSystem FileSystems}. If the {@code FileSystem} this {@code Node} belongs to does not support the + * {@link FileSystemFeature#CREATION_TIME_FEATURE} the behavior of this method is unspecified. + * + * @returns the creation time of the file. + * @see FileSystem#supports(Class) + */ + default Instant creationTime() throws UncheckedIOException { + throw new UncheckedIOException(new IOException("CreationTime not supported")); + } + /** * @return the {@link FileSystem} this Node belongs to */ 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 9e44eb8a8..e14e6987f 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 @@ -24,6 +24,20 @@ public interface WritableFile extends WritableByteChannel { void setLastModified(Instant instant) throws UncheckedIOException; + /** + *
+ * Sets the creation time of the file. + *
+ * Setting the creation time may not be supported by all {@link FileSystem FileSystems}. If the {@code FileSystem} this {@code WritableFile} belongs to does not support the + * {@link FileSystemFeature#CREATION_TIME_FEATURE} the behavior of this method is unspecified. + * + * @param instant the time to set as creation time + * @see FileSystem#supports(Class) + */ + default void setCreationTime(Instant instant) throws UncheckedIOException { + throw new UncheckedIOException(new IOException("CreationTime not supported")); + } + /** *
* Deletes this file from the file system. diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java index 960bf5389..de763a208 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java @@ -9,6 +9,7 @@ package org.cryptomator.filesystem.delegating; import java.io.UncheckedIOException; +import java.time.Instant; import java.util.Optional; import java.util.stream.Stream; @@ -90,4 +91,9 @@ public abstract class DelegatingFolder implements Node { return delegate.lastModified(); } + @Override + public Instant creationTime() throws UncheckedIOException { + return delegate.creationTime(); + } + @Override public int hashCode() { return delegate.hashCode(); diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingWritableFile.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingWritableFile.java index 905153b7a..66bd05712 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingWritableFile.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingWritableFile.java @@ -67,4 +67,9 @@ public class DelegatingWritableFile implements WritableFile { delegate.close(); } + @Override + public void setCreationTime(Instant instant) throws UncheckedIOException { + delegate.setCreationTime(instant); + } + } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFileSystem.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFileSystem.java index 9d182b3bb..636c3e216 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFileSystem.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFileSystem.java @@ -9,6 +9,7 @@ package org.cryptomator.filesystem.blockaligned; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; class BlockAlignedFileSystem extends BlockAlignedFolder implements FileSystem { @@ -17,4 +18,9 @@ class BlockAlignedFileSystem extends BlockAlignedFolder implements FileSystem { super(null, delegate, blockSize); } + @Override + public boolean supports(FileSystemFeature feature) { + return delegate.fileSystem().supports(feature); + } + } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java index 9c60ffeb8..8c3402418 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java @@ -54,4 +54,9 @@ public class CryptoFile extends CryptoNode implements File { return toString().compareTo(o.toString()); } + @Override + public Instant creationTime() throws UncheckedIOException { + return physicalFile().creationTime(); + } + } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java index b19555e20..f4bccb75a 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java @@ -15,6 +15,7 @@ import org.cryptomator.crypto.engine.Cryptor; import org.cryptomator.crypto.engine.InvalidPassphraseException; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; @@ -104,6 +105,11 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem { physicalFolder().create(); } + @Override + public boolean supports(FileSystemFeature feature) { + return physicalRoot.fileSystem().supports(feature); + } + @Override public String toString() { return physicalRoot + ":::/"; diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java index a07105b30..bd9902e77 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java @@ -146,6 +146,11 @@ class CryptoFolder extends CryptoNode implements Folder { } + @Override + public Instant creationTime() throws UncheckedIOException { + return physicalFile().creationTime(); + } + @Override public String toString() { return parent.toString() + name + "/"; diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java index bd2bc5faa..443bd6737 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java @@ -93,6 +93,11 @@ class CryptoWritableFile implements WritableFile { throw new UnsupportedOperationException("Truncate not supported yet"); } + @Override + public void setCreationTime(Instant instant) throws UncheckedIOException { + file.setCreationTime(instant); + } + @Override public boolean isOpen() { return file.isOpen(); diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java index b3ce61b1f..24c7dbddd 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java @@ -26,8 +26,8 @@ class InMemoryFile extends InMemoryNode implements File { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private ByteBuffer content = ByteBuffer.allocate(0); - public InMemoryFile(InMemoryFolder parent, String name, Instant lastModified) { - super(parent, name, lastModified); + public InMemoryFile(InMemoryFolder parent, String name, Instant lastModified, Instant creationTime) { + super(parent, name, lastModified, creationTime); } @Override @@ -48,19 +48,24 @@ class InMemoryFile extends InMemoryNode implements File { parent.children.compute(this.name(), (k, v) -> { if (v == null || v == this) { this.lastModified = Instant.now(); + this.creationTime = Instant.now(); return this; } else { throw new UncheckedIOException(new FileExistsException(k)); } }); parent.volatileFiles.remove(name); - return new InMemoryWritableFile(this::setLastModified, this::getContent, this::setContent, this::delete, writeLock); + return new InMemoryWritableFile(this::setLastModified, this::setCreationTime, this::getContent, this::setContent, this::delete, writeLock); } private void setLastModified(Instant lastModified) { this.lastModified = lastModified; } + private void setCreationTime(Instant creationTime) { + this.creationTime = creationTime; + } + private ByteBuffer getContent() { return content; } @@ -75,7 +80,7 @@ class InMemoryFile extends InMemoryNode implements File { // returning null removes the entry. return null; }); - assert!this.exists(); + assert !this.exists(); } @Override diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java index bc1b8ce3f..8b4fc35c7 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java @@ -12,11 +12,12 @@ import java.time.Instant; import java.util.Optional; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; public class InMemoryFileSystem extends InMemoryFolder implements FileSystem { public InMemoryFileSystem() { - super(null, "", Instant.now()); + super(null, "", Instant.now(), Instant.now()); } @Override @@ -39,4 +40,9 @@ public class InMemoryFileSystem extends InMemoryFolder implements FileSystem { return "/"; } + @Override + public boolean supports(FileSystemFeature feature) { + return feature == FileSystemFeature.CREATION_TIME_FEATURE; + } + } diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java index 509fdd77e..fc3d9b979 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java @@ -25,8 +25,8 @@ class InMemoryFolder extends InMemoryNode implements Folder { final Map volatileFiles = new HashMap<>(); final Map volatileFolders = new HashMap<>(); - public InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified) { - super(parent, name, lastModified); + public InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified, Instant creationTime) { + super(parent, name, lastModified, creationTime); } @Override @@ -41,7 +41,7 @@ class InMemoryFolder extends InMemoryNode implements Folder { return (InMemoryFile) node; } else { return volatileFiles.computeIfAbsent(name, (n) -> { - return new InMemoryFile(this, n, Instant.MIN); + return new InMemoryFile(this, n, Instant.MIN, Instant.MIN); }); } } @@ -53,7 +53,7 @@ class InMemoryFolder extends InMemoryNode implements Folder { return (InMemoryFolder) node; } else { return volatileFolders.computeIfAbsent(name, (n) -> { - return new InMemoryFolder(this, n, Instant.MIN); + return new InMemoryFolder(this, n, Instant.MIN, Instant.MIN); }); } } @@ -74,6 +74,7 @@ class InMemoryFolder extends InMemoryNode implements Folder { }); parent.volatileFolders.remove(name); assert this.exists(); + creationTime = Instant.now(); } @Override @@ -81,11 +82,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 @@ -107,7 +108,7 @@ class InMemoryFolder extends InMemoryNode implements Folder { subFolder.delete(); } } - assert!this.exists(); + assert !this.exists(); } @Override 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 fbe899529..ea106b472 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 @@ -8,6 +8,8 @@ *******************************************************************************/ package org.cryptomator.filesystem.inmem; +import java.io.IOException; +import java.io.UncheckedIOException; import java.time.Instant; import java.util.Optional; @@ -18,11 +20,13 @@ class InMemoryNode implements Node { protected final InMemoryFolder parent; protected final String name; protected Instant lastModified; + protected Instant creationTime; - public InMemoryNode(InMemoryFolder parent, String name, Instant lastModified) { + public InMemoryNode(InMemoryFolder parent, String name, Instant lastModified, Instant creationTime) { this.parent = parent; this.name = name; this.lastModified = lastModified; + this.creationTime = creationTime; } @Override @@ -66,4 +70,13 @@ class InMemoryNode implements Node { } } + @Override + public Instant creationTime() throws UncheckedIOException { + if (exists()) { + return creationTime; + } else { + throw new UncheckedIOException(new IOException("Node does not exist")); + } + } + } diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryWritableFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryWritableFile.java index d12a83412..f621f6696 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryWritableFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryWritableFile.java @@ -21,6 +21,7 @@ import org.cryptomator.io.ByteBuffers; public class InMemoryWritableFile implements WritableFile { private final Consumer lastModifiedSetter; + private final Consumer creationTimeSetter; private final Supplier contentGetter; private final Consumer contentSetter; private final Consumer deleter; @@ -29,12 +30,14 @@ public class InMemoryWritableFile implements WritableFile { private boolean open; private int position = 0; - public InMemoryWritableFile(Consumer lastModifiedSetter, Supplier contentGetter, Consumer contentSetter, Consumer deleter, WriteLock writeLock) { + public InMemoryWritableFile(Consumer lastModifiedSetter, Consumer creationTimeSetter, Supplier contentGetter, Consumer contentSetter, Consumer deleter, + WriteLock writeLock) { this.lastModifiedSetter = lastModifiedSetter; this.contentGetter = contentGetter; this.contentSetter = contentSetter; this.deleter = deleter; this.writeLock = writeLock; + this.creationTimeSetter = creationTimeSetter; } @Override @@ -98,4 +101,9 @@ public class InMemoryWritableFile implements WritableFile { lastModifiedSetter.accept(Instant.now()); } + @Override + public void setCreationTime(Instant instant) throws UncheckedIOException { + creationTimeSetter.accept(instant); + } + } diff --git a/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileTest.java b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileTest.java new file mode 100644 index 000000000..8f19f811a --- /dev/null +++ b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileTest.java @@ -0,0 +1,56 @@ +package org.cryptomator.filesystem.inmem; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.io.UncheckedIOException; +import java.time.Instant; + +import org.cryptomator.filesystem.WritableFile; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class InMemoryFileTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testCreationTimeOfNonExistingFileThrowsUncheckedIOException() { + InMemoryFileSystem fileSystem = new InMemoryFileSystem(); + InMemoryFile inTest = fileSystem.file("foo"); + + thrown.expect(UncheckedIOException.class); + + inTest.creationTime(); + } + + @Test + public void testCreationTimeOfCreatedFileIsSetToInstantDuringCreation() { + InMemoryFileSystem fileSystem = new InMemoryFileSystem(); + InMemoryFile inTest = fileSystem.file("foo"); + + Instant minCreationTime = Instant.now(); + Instant maxCreationTime; + try (WritableFile writable = inTest.openWritable()) { + maxCreationTime = Instant.now(); + } + + assertThat(inTest.creationTime().isBefore(minCreationTime), is(false)); + assertThat(inTest.creationTime().isAfter(maxCreationTime), is(false)); + } + + @Test + public void testCreationTimeSetInWritableFileIsSaved() { + Instant creationTime = Instant.parse("2015-03-23T21:11:32Z"); + InMemoryFileSystem fileSystem = new InMemoryFileSystem(); + InMemoryFile inTest = fileSystem.file("foo"); + try (WritableFile writable = inTest.openWritable()) { + writable.setCreationTime(creationTime); + } + + assertThat(inTest.creationTime(), is(creationTime)); + } + +} diff --git a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFileSystem.java b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFileSystem.java index 23786805a..74bf3ca54 100644 --- a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFileSystem.java +++ b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFileSystem.java @@ -3,6 +3,7 @@ package org.cryptomator.filesystem.blacklisting; import java.util.function.Predicate; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.Node; @@ -12,4 +13,9 @@ class BlacklistingFileSystem extends BlacklistingFolder implements FileSystem { super(null, root, hiddenNodes); } + @Override + public boolean supports(FileSystemFeature feature) { + return delegate.fileSystem().supports(feature); + } + } diff --git a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFileSystem.java b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFileSystem.java index 0a5f8f1b3..379aced28 100644 --- a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFileSystem.java +++ b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFileSystem.java @@ -1,6 +1,7 @@ package org.cryptomator.filesystem.shortening; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; class ShorteningFileSystem extends ShorteningFolder implements FileSystem { @@ -9,4 +10,9 @@ class ShorteningFileSystem extends ShorteningFolder implements FileSystem { super(null, root, "", new FilenameShortener(metadataRoot, threshold)); } + @Override + public boolean supports(FileSystemFeature feature) { + return delegate.fileSystem().supports(feature); + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java index 9e1518e27..b57b07ece 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioAccess.java @@ -8,12 +8,19 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.OpenOption; import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileTime; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + class DefaultNioAccess implements NioAccess { + private static final Logger LOG = LoggerFactory.getLogger(DefaultNioAccess.class); + @Override public FileChannel open(Path path, OpenOption... options) throws IOException { return FileChannel.open(path, options); @@ -74,4 +81,30 @@ class DefaultNioAccess implements NioAccess { return FileSystems.getDefault().getSeparator(); } + @Override + public FileTime getCreationTime(Path path, LinkOption... options) throws IOException { + return Files.readAttributes(path, BasicFileAttributes.class, options).creationTime(); + } + + @Override + public void setCreationTime(Path path, FileTime creationTime, LinkOption... options) throws IOException { + Files.getFileAttributeView(path, BasicFileAttributeView.class, options).setTimes(null, null, creationTime); + } + + @Override + public boolean supportsCreationTime(Path path) { + try { + Path file = Files.createTempFile(path, "creationTimeCheck", "tmp"); + long expected = 1184725140000L; + long millisecondsInADay = 86400000L; + FileTime fileTime = FileTime.fromMillis(expected); + Files.getFileAttributeView(file, BasicFileAttributeView.class).setTimes(null, null, fileTime); + long actual = Files.readAttributes(file, BasicFileAttributes.class).creationTime().toMillis(); + return Math.abs(expected - actual) <= millisecondsInADay; + } catch (IOException e) { + LOG.info("supportsCreationTime failed", e); + return false; + } + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java index e8cb7004f..1d98513e5 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioAccess.java @@ -40,4 +40,10 @@ interface NioAccess { String separator(); + FileTime getCreationTime(Path path, LinkOption... options) throws IOException; + + void setCreationTime(Path path, FileTime creationTime, LinkOption... options) throws IOException; + + boolean supportsCreationTime(Path path); + } 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 195e6e0bf..1d7fa0927 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 @@ -82,4 +82,12 @@ class NioFile extends NioNode implements File { return format("NioFile(%s)", path); } + @Override + public Instant creationTime() throws UncheckedIOException { + if (nioAccess.exists(path) && !exists()) { + throw new UncheckedIOException(new IOException(format("%s is a folder", path))); + } + return super.creationTime(); + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java index 827c75ec5..c2566abd0 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java @@ -2,11 +2,16 @@ package org.cryptomator.filesystem.nio; import java.nio.file.Path; import java.util.Optional; +import java.util.function.Supplier; +import org.cryptomator.common.CachingSupplier; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.FileSystemFeature; public class NioFileSystem extends NioFolder implements FileSystem { + private final Supplier supportsCreationTime = CachingSupplier.from(this::supportsCreationTime); + public static NioFileSystem rootedAt(Path root) { return new NioFileSystem(root); } @@ -16,4 +21,17 @@ public class NioFileSystem extends NioFolder implements FileSystem { create(); } + @Override + public boolean supports(FileSystemFeature feature) { + if (feature == FileSystemFeature.CREATION_TIME_FEATURE) { + return supportsCreationTime.get(); + } else { + return false; + } + } + + private boolean supportsCreationTime() { + return nioAccess.supportsCreationTime(path); + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java index bd83bf17b..74587e235 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java @@ -111,6 +111,14 @@ class NioFolder extends NioNode implements Folder { return path; } + @Override + public Instant creationTime() throws UncheckedIOException { + if (nioAccess.exists(path) && !nioAccess.isDirectory(path)) { + throw new UncheckedIOException(new IOException(format("%s is a file", path))); + } + return super.creationTime(); + } + @Override public String toString() { return format("NioFolder(%s)", path); diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java index 9fb0a972a..ae6d2fedd 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java @@ -43,4 +43,13 @@ abstract class NioNode implements Node { } } + @Override + public Instant creationTime() throws UncheckedIOException { + try { + return nioAccess.getCreationTime(path).toInstant(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } 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 index c6670431c..9b1968b6b 100644 --- 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 @@ -128,6 +128,17 @@ class WritableNioFile implements WritableFile { channel.truncate(0); } + @Override + public void setCreationTime(Instant instant) throws UncheckedIOException { + assertOpen(); + ensureChannelIsOpened(); + try { + nioAccess.setCreationTime(path, FileTime.from(instant)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + @Override public void close() throws UncheckedIOException { if (!open) { diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/DefaultNioAccessTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/DefaultNioAccessTest.java index d2caa1b2b..0f50c012c 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/DefaultNioAccessTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/DefaultNioAccessTest.java @@ -16,6 +16,7 @@ import static org.mockito.Mockito.when; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; @@ -34,7 +35,12 @@ import java.util.HashSet; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import de.bechte.junit.runners.context.HierarchicalContextRunner; + +@RunWith(HierarchicalContextRunner.class) public class DefaultNioAccessTest { private DefaultNioAccess inTest = new DefaultNioAccess(); @@ -186,6 +192,159 @@ public class DefaultNioAccessTest { verify(implCloseChannel).run(); } + @Test + public void testGetCreationTimeReadsAttributesUsingProviderAndReturnsValueFromThem() throws IOException { + FileTime expectedValue = FileTime.from(Instant.parse("2016-01-08T22:32:00Z")); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(attributes.creationTime()).thenReturn(expectedValue); + LinkOption[] options = {LinkOption.NOFOLLOW_LINKS}; + when(provider.readAttributes(path, BasicFileAttributes.class, options)).thenReturn(attributes); + + FileTime result = inTest.getCreationTime(path, options); + + assertThat(result, is(expectedValue)); + } + + @Test + public void testSetCreationTimeGetsAttributeViewUsingProviderAndSetsCreationTimeUsingIt() throws IOException { + FileTime fileTime = FileTime.from(Instant.now()); + BasicFileAttributeView attributes = mock(BasicFileAttributeView.class); + LinkOption[] options = {LinkOption.NOFOLLOW_LINKS}; + when(provider.getFileAttributeView(path, BasicFileAttributeView.class, options)).thenReturn(attributes); + + inTest.setCreationTime(path, fileTime, options); + + verify(attributes).setTimes(null, null, fileTime); + } + + public class SupportsCreationTimeTests { + + @Test + public void testSupportsCreationTimeReturnsTrueIfGetCreationTimeIsADayOfFromSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet + millisecondsInADay)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(true)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeReturnsTrueIfGetCreationTimeIsADayOfBeforeSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet - millisecondsInADay)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(true)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeSucceedsIfGetCreationTimeIsADayOfBeforeSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet - millisecondsInADay)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(true)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeReturnsFalseIfGetCreationTimeIsMoreAsADayOfFromSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet + millisecondsInADay + 1)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(false)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeReturnsFalseIfGetCreationTimeIsMoreAsADayBeforeSetCreationTime() throws IOException { + long expectedMillisSet = 1184725140000L; + long millisecondsInADay = 86400000L; + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenReturn(mock(SeekableByteChannel.class)); + BasicFileAttributeView attributesView = mock(BasicFileAttributeView.class); + BasicFileAttributes attributes = mock(BasicFileAttributes.class); + when(provider.getFileAttributeView(tempFile, BasicFileAttributeView.class)).thenReturn(attributesView); + when(provider.readAttributes(tempFile, BasicFileAttributes.class)).thenReturn(attributes); + when(attributes.creationTime()).thenReturn(FileTime.fromMillis(expectedMillisSet - millisecondsInADay - 1)); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(false)); + verify(attributesView).setTimes(null, null, FileTime.fromMillis(expectedMillisSet)); + } + + @Test + public void testSupportsCreationTimeReturnsFalseIfIOExceptionOccurs() throws IOException { + Path tempFileName = mock(Path.class); + Path tempFile = mock(Path.class); + when(tempFile.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.getPath(Matchers.any())).thenReturn(tempFileName); + when(path.resolve(tempFileName)).thenReturn(tempFile); + when(provider.newByteChannel(any(), any())).thenThrow(new IOException()); + + boolean result = inTest.supportsCreationTime(path); + + assertThat(result, is(false)); + } + + } + @Test public void testSeparatorReturnsSeparatorOfDefaultFileSystem() { assertThat(inTest.separator(), is(FileSystems.getDefault().getSeparator())); diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java index 07ef1eb66..916797ba2 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Optional; +import org.cryptomator.filesystem.FileSystemFeature; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -51,6 +52,31 @@ public class NioFileSystemTest { verify(nioAccess).createDirectories(path); } + @Test + public void testSupportsCreationTimeDelegatesToNioAccessWithTrue() { + when(nioAccess.supportsCreationTime(path)).thenReturn(true); + + boolean result = inTest.supports(FileSystemFeature.CREATION_TIME_FEATURE); + + assertThat(result, is(true)); + } + + @Test + public void testSupportsWithOtherFeatureReturnsFalse() { + boolean result = inTest.supports(null); + + assertThat(result, is(false)); + } + + @Test + public void testSupportsCreationTimeDelegatesToNioAccessWithFalse() { + when(nioAccess.supportsCreationTime(path)).thenReturn(false); + + boolean result = inTest.supports(FileSystemFeature.CREATION_TIME_FEATURE); + + assertThat(result, is(false)); + } + @After public void tearDown() { InstanceFactory.DEFAULT.reset(); 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 index 09460c007..3b0ee216c 100644 --- 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 @@ -295,6 +295,46 @@ public class NioFileTest { } + public class CreationTime { + + @Test + public void testCreationTimeDelegatesToNioAccessCreationTime() throws IOException { + Instant exectedResult = Instant.parse("2016-01-08T19:49:00Z"); + when(nioAccess.getCreationTime(path)).thenReturn(FileTime.from(exectedResult)); + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isRegularFile(path)).thenReturn(true); + + Instant result = inTest.creationTime(); + + assertThat(result, is(exectedResult)); + } + + @Test + public void testCreationTimeWrapsIOExceptionFromNioAccessCreationTimeInUncheckedIOException() throws IOException { + IOException exceptionFromCreationTime = new IOException(); + when(nioAccess.getCreationTime(path)).thenThrow(exceptionFromCreationTime); + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isRegularFile(path)).thenReturn(true); + + thrown.expect(UncheckedIOException.class); + thrown.expectCause(is(exceptionFromCreationTime)); + + inTest.creationTime(); + } + + @Test + public void testCreationTimeThrowsExceptionIfFileIsNoRegularFile() { + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isRegularFile(path)).thenReturn(false); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage(format("%s is a folder", path)); + + inTest.creationTime(); + } + + } + @Test public void testNameReturnsFileNameOfPath() { Path fileName = mock(Path.class); 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 8b74ff94e..e4404dcee 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 @@ -410,6 +410,57 @@ public class NioFolderTest { } + public class CreationTimeTests { + + @Test + public void testCreationTimeDelegatesToNioAccessCreationTimeForExistingFolder() throws IOException { + Instant exectedResult = Instant.parse("2016-01-08T19:49:00Z"); + when(nioAccess.getCreationTime(path)).thenReturn(FileTime.from(exectedResult)); + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isDirectory(path)).thenReturn(true); + + Instant result = inTest.creationTime(); + + assertThat(result, is(exectedResult)); + } + + @Test + public void testCreationTimeDelegatesToNioAccessCreationTimeForNonExistingFolder() throws IOException { + Instant exectedResult = Instant.parse("2016-01-08T19:49:00Z"); + when(nioAccess.getCreationTime(path)).thenReturn(FileTime.from(exectedResult)); + when(nioAccess.exists(path)).thenReturn(false); + + Instant result = inTest.creationTime(); + + assertThat(result, is(exectedResult)); + } + + @Test + public void testCreationTimeWrapsIOExceptionFromNioAccessCreationTimeInUncheckedIOException() throws IOException { + IOException exceptionFromCreationTime = new IOException(); + when(nioAccess.getCreationTime(path)).thenThrow(exceptionFromCreationTime); + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isDirectory(path)).thenReturn(true); + + thrown.expect(UncheckedIOException.class); + thrown.expectCause(is(exceptionFromCreationTime)); + + inTest.creationTime(); + } + + @Test + public void testCreationTimeThrowsExceptionIfFileIsNoDirectory() { + when(nioAccess.exists(path)).thenReturn(true); + when(nioAccess.isDirectory(path)).thenReturn(false); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage(format("%s is a file", path)); + + inTest.creationTime(); + } + + } + public class DeleteTests { @Test diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WritableNioFileTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WritableNioFileTest.java index 6d55e6b64..6027ef59b 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WritableNioFileTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WritableNioFileTest.java @@ -5,7 +5,9 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static org.cryptomator.filesystem.nio.OpenMode.WRITE; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.same; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -286,6 +288,44 @@ public class WritableNioFileTest { } + public class SetCreationTimeTests { + + @Test + public void testSetCreationTimeFailsIfNotOpen() { + Instant irrelevant = null; + inTest.close(); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage("already closed"); + + inTest.setCreationTime(irrelevant); + } + + @Test + public void testSetCreationTimeOpensChannelIfClosedAndInvokesNioAccessSetCreationTimeAfterwards() throws IOException { + Instant instant = Instant.parse("2016-01-08T22:32:00Z"); + + inTest.setCreationTime(instant); + + InOrder inOrder = inOrder(nioAccess, channel); + inOrder.verify(channel).openIfClosed(OpenMode.WRITE); + inOrder.verify(nioAccess).setCreationTime(path, FileTime.from(instant)); + } + + @Test + public void testSetCreationTimeWrapsIOExceptionFromSetCreationTimeInUncheckedIOException() throws IOException { + IOException exceptionFromSetCreationTime = new IOException(); + Instant irrelevant = Instant.now(); + doThrow(exceptionFromSetCreationTime).when(nioAccess).setCreationTime(same(path), any()); + + thrown.expect(UncheckedIOException.class); + thrown.expectCause(is(exceptionFromSetCreationTime)); + + inTest.setCreationTime(irrelevant); + } + + } + public class DeleteTests { @Test diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java index 7aa0a17d6..40facff16 100644 --- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFile.java @@ -21,6 +21,7 @@ import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.io.InputContext; import org.apache.jackrabbit.webdav.io.OutputContext; import org.apache.jackrabbit.webdav.lock.LockManager; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; import org.cryptomator.filesystem.jackrabbit.FileLocator; @@ -98,8 +99,11 @@ class DavFile extends DavNode { @Override protected void setCreationTime(Instant instant) { - // TODO Auto-generated method stub - + if (node.fileSystem().supports(FileSystemFeature.CREATION_TIME_FEATURE)) { + try (WritableFile writable = node.openWritable()) { + writable.setCreationTime(instant); + } + } } } diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java index 98969a353..54c819bd5 100644 --- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavFolder.java @@ -31,6 +31,7 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import org.apache.jackrabbit.webdav.property.ResourceType; import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.Node; import org.cryptomator.filesystem.WritableFile; @@ -154,8 +155,9 @@ class DavFolder extends DavNode { @Override protected void setCreationTime(Instant instant) { - // TODO Auto-generated method stub - + if (node.fileSystem().supports(FileSystemFeature.CREATION_TIME_FEATURE)) { + node.setCreationTime(instant); + } } } diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavNode.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavNode.java index 71be3e145..152358394 100644 --- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavNode.java +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbitservlet/DavNode.java @@ -28,7 +28,9 @@ import org.apache.jackrabbit.webdav.property.DavProperty; import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import org.apache.jackrabbit.webdav.property.PropEntry; +import org.cryptomator.filesystem.FileSystemFeature; import org.cryptomator.filesystem.jackrabbit.FileSystemResourceLocator; abstract class DavNode implements DavResource { @@ -106,7 +108,16 @@ abstract class DavNode implements DavResour @Override public DavProperty> getProperty(DavPropertyName name) { - return getProperties().get(name); + final String namespacelessPropertyName = name.getName(); + if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName)) { + if (node.fileSystem().supports(FileSystemFeature.CREATION_TIME_FEATURE)) { + return new DefaultDavProperty<>(name, DateTimeFormatter.RFC_1123_DATE_TIME.format(node.creationTime())); + } else { + return null; + } + } else { + return getProperties().get(name); + } } @Override @@ -116,8 +127,6 @@ abstract class DavNode implements DavResour @Override public void setProperty(DavProperty> property) throws DavException { - getProperties().add(property); - final String namespacelessPropertyName = property.getName().getName(); if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) { final String createDateStr = (String) property.getValue(); @@ -127,6 +136,9 @@ abstract class DavNode implements DavResour final String lastModifiedTimeStr = (String) property.getValue(); final Instant modificationTime = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(lastModifiedTimeStr)); this.setModificationTime(modificationTime); + getProperties().add(property); + } else { + getProperties().add(property); } }