Added creation time

* Getter and setter for files and folders
* A way to determine if a file system supports creation dates
* WebDav compliant implementation in jackrabbit-adapter
* Tests
This commit is contained in:
Markus Kreusch
2016-01-09 00:51:25 +01:00
parent a746a73667
commit 415423abd7
40 changed files with 735 additions and 24 deletions

View File

@@ -31,6 +31,11 @@
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -12,11 +12,24 @@
<artifactId>commons</artifactId>
<name>Cryptomator common</name>
<description>Shared utilities</description>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
</dependency>
</dependencies>

View File

@@ -0,0 +1,26 @@
package org.cryptomator.common;
import java.util.function.Supplier;
public class CachingSupplier<T> implements Supplier<T> {
public static <T> Supplier<T> from(Supplier<T> delegate) {
return new CachingSupplier<>(delegate);
}
private Supplier<T> delegate;
private CachingSupplier(Supplier<T> delegate) {
this.delegate = () -> {
T result = delegate.get();
CachingSupplier.this.delegate = () -> result;
return result;
};
}
@Override
public T get() {
return delegate.get();
}
}

View File

@@ -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<Object> delegate = mock(Supplier.class);
Object expectedResult = new Object();
when(delegate.get()).thenReturn(expectedResult);
Supplier<Object> inTest = CachingSupplier.from(delegate);
Object result = inTest.get();
assertThat(result, is(expectedResult));
}
@Test
public void testInvokingGetTwiceDoesNotInvokeDelegateTwice() {
@SuppressWarnings("unchecked")
Supplier<Object> delegate = mock(Supplier.class);
Object expectedResult = new Object();
when(delegate.get()).thenReturn(expectedResult);
Supplier<Object> inTest = CachingSupplier.from(delegate);
inTest.get();
Object result = inTest.get();
assertThat(result, is(expectedResult));
verify(delegate).get();
}
}

View File

@@ -23,4 +23,8 @@ public interface FileSystem extends Folder {
return Optional.empty();
}
default boolean supports(FileSystemFeature feature) {
return false;
}
}

View File

@@ -0,0 +1,5 @@
package org.cryptomator.filesystem;
public enum FileSystemFeature {
CREATION_TIME_FEATURE
}

View File

@@ -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 {
}
}
/**
* <p>
* Sets the creation time of the folder.
* <p>
* 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"));
}
}

View File

@@ -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;
/**
* <p>
* Determines the creation time of this node.
* <p>
* 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
*/

View File

@@ -24,6 +24,20 @@ public interface WritableFile extends WritableByteChannel {
void setLastModified(Instant instant) throws UncheckedIOException;
/**
* <p>
* Sets the creation time of the file.
* <p>
* 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"));
}
/**
* <p>
* Deletes this file from the file system.

View File

@@ -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<R extends DelegatingReadableFile, W exten
}
}
@Override
public void setCreationTime(Instant instant) throws UncheckedIOException {
delegate.setCreationTime(instant);
}
}

View File

@@ -39,6 +39,11 @@ abstract class DelegatingNode<T extends Node> implements Node {
return delegate.lastModified();
}
@Override
public Instant creationTime() throws UncheckedIOException {
return delegate.creationTime();
}
@Override
public int hashCode() {
return delegate.hashCode();

View File

@@ -67,4 +67,9 @@ public class DelegatingWritableFile implements WritableFile {
delegate.close();
}
@Override
public void setCreationTime(Instant instant) throws UncheckedIOException {
delegate.setCreationTime(instant);
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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 + ":::/";

View File

@@ -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 + "/";

View File

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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -25,8 +25,8 @@ class InMemoryFolder extends InMemoryNode implements Folder {
final Map<String, InMemoryFile> volatileFiles = new HashMap<>();
final Map<String, InMemoryFolder> 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

View File

@@ -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"));
}
}
}

View File

@@ -21,6 +21,7 @@ import org.cryptomator.io.ByteBuffers;
public class InMemoryWritableFile implements WritableFile {
private final Consumer<Instant> lastModifiedSetter;
private final Consumer<Instant> creationTimeSetter;
private final Supplier<ByteBuffer> contentGetter;
private final Consumer<ByteBuffer> contentSetter;
private final Consumer<Void> deleter;
@@ -29,12 +30,14 @@ public class InMemoryWritableFile implements WritableFile {
private boolean open;
private int position = 0;
public InMemoryWritableFile(Consumer<Instant> lastModifiedSetter, Supplier<ByteBuffer> contentGetter, Consumer<ByteBuffer> contentSetter, Consumer<Void> deleter, WriteLock writeLock) {
public InMemoryWritableFile(Consumer<Instant> lastModifiedSetter, Consumer<Instant> creationTimeSetter, Supplier<ByteBuffer> contentGetter, Consumer<ByteBuffer> contentSetter, Consumer<Void> 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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

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

View File

@@ -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<Boolean> 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);
}
}

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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<FileLocator> {
@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);
}
}
}
}

View File

@@ -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<FolderLocator> {
@Override
protected void setCreationTime(Instant instant) {
// TODO Auto-generated method stub
if (node.fileSystem().supports(FileSystemFeature.CREATION_TIME_FEATURE)) {
node.setCreationTime(instant);
}
}
}

View File

@@ -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<T extends FileSystemResourceLocator> implements DavResource {
@@ -106,7 +108,16 @@ abstract class DavNode<T extends FileSystemResourceLocator> 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<T extends FileSystemResourceLocator> 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<T extends FileSystemResourceLocator> 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);
}
}