Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit d82337d

Browse files
committed
Merge branch 'develop' into release/0.3.0
2 parents 677ec22 + 2f8058c commit d82337d

File tree

12 files changed

+128
-112
lines changed

12 files changed

+128
-112
lines changed

src/main/java/org/cryptomator/cloudaccess/MetadataCachingProviderDecorator.java

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@
1111

1212
import java.io.InputStream;
1313
import java.time.Duration;
14+
import java.time.Instant;
1415
import java.util.Optional;
1516
import java.util.concurrent.CompletableFuture;
1617
import java.util.concurrent.CompletionStage;
1718
import java.util.function.Function;
1819

1920
public class MetadataCachingProviderDecorator implements CloudProvider {
2021

21-
private final CloudProvider delegate;
2222
final Cache<CloudPath, Optional<CloudItemMetadata>> metadataCache;
23+
private final CloudProvider delegate;
2324

2425
public MetadataCachingProviderDecorator(CloudProvider delegate) {
2526
this(delegate, Duration.ofSeconds(10));
@@ -99,19 +100,16 @@ public CompletionStage<InputStream> read(CloudPath file, long offset, long count
99100
}
100101

101102
@Override
102-
public CompletionStage<CloudItemMetadata> write(CloudPath file, boolean replace, InputStream data, long size, ProgressListener progressListener) {
103-
return delegate.write(file, replace, data, size, progressListener).thenApply(metadata -> {
104-
metadataCache.put(file, Optional.of(metadata));
105-
return metadata;
106-
}).handle((metadata, exception) -> {
107-
if (exception == null) {
108-
assert metadata != null;
109-
return CompletableFuture.completedFuture(metadata);
110-
} else {
111-
metadataCache.invalidate(file);
112-
return CompletableFuture.<CloudItemMetadata>failedFuture(exception);
113-
}
114-
}).thenCompose(Function.identity());
103+
public CompletionStage<Void> write(CloudPath file, boolean replace, InputStream data, long size, Optional<Instant> lastModified, ProgressListener progressListener) {
104+
return delegate.write(file, replace, data, size, lastModified, progressListener)
105+
.handle((nullReturn, exception) -> {
106+
if (exception == null) {
107+
return CompletableFuture.completedFuture(nullReturn);
108+
} else {
109+
metadataCache.invalidate(file);
110+
return CompletableFuture.<Void>failedFuture(exception);
111+
}
112+
}).thenCompose(Function.identity());
115113
}
116114

117115
@Override
@@ -156,8 +154,8 @@ public CompletionStage<CloudPath> move(CloudPath source, CloudPath target, boole
156154
}
157155

158156
private void evictIncludingDescendants(CloudPath cleartextPath) {
159-
for(var path : metadataCache.asMap().keySet()) {
160-
if(path.startsWith(cleartextPath)) {
157+
for (var path : metadataCache.asMap().keySet()) {
158+
if (path.startsWith(cleartextPath)) {
161159
metadataCache.invalidate(path);
162160
}
163161
}

src/main/java/org/cryptomator/cloudaccess/api/CloudProvider.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
55

66
import java.io.InputStream;
7+
import java.time.Instant;
78
import java.util.Optional;
89
import java.util.concurrent.CompletableFuture;
910
import java.util.concurrent.CompletionStage;
@@ -107,7 +108,7 @@ default CompletionStage<InputStream> read(CloudPath file, ProgressListener progr
107108
CompletionStage<InputStream> read(CloudPath file, long offset, long count, ProgressListener progressListener);
108109

109110
/**
110-
* Writes to a given file, creating it if it doesn't exist yet.
111+
* Writes to a given file, creating it if it doesn't exist yet. <code>lastModified</code> is applied with best-effort but without guarantee.
111112
* <p>
112113
* The returned CompletionStage might complete exceptionally with one of the following exceptions:
113114
* <ul>
@@ -121,10 +122,11 @@ default CompletionStage<InputStream> read(CloudPath file, ProgressListener progr
121122
* @param replace Flag indicating whether to overwrite the file if it already exists.
122123
* @param data A data source from which to copy contents to the remote file
123124
* @param size The size of data
125+
* @param lastModified The lastModified which should be provided to the server
124126
* @param progressListener TODO Future use
125-
* @return CompletionStage that will be completed after writing all <code>data</code> and holds the new metadata of the item referenced by <code>file</code>.
127+
* @return CompletionStage that will be completed after writing all <code>data</code>.
126128
*/
127-
CompletionStage<CloudItemMetadata> write(CloudPath file, boolean replace, InputStream data, long size, ProgressListener progressListener);
129+
CompletionStage<Void> write(CloudPath file, boolean replace, InputStream data, long size, Optional<Instant> lastModified, ProgressListener progressListener);
128130

129131
/**
130132
* Create a folder. Does not create any potentially missing parent directories.

src/main/java/org/cryptomator/cloudaccess/localfs/LocalFsCloudProvider.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import java.nio.file.StandardCopyOption;
3232
import java.nio.file.StandardOpenOption;
3333
import java.nio.file.attribute.BasicFileAttributes;
34+
import java.nio.file.attribute.FileTime;
35+
import java.time.Instant;
3436
import java.util.ArrayList;
3537
import java.util.EnumSet;
3638
import java.util.List;
@@ -142,7 +144,7 @@ public CompletionStage<InputStream> read(CloudPath file, long offset, long count
142144
}
143145

144146
@Override
145-
public CompletionStage<CloudItemMetadata> write(CloudPath file, boolean replace, InputStream data, long size, ProgressListener progressListener) {
147+
public CompletionStage<Void> write(CloudPath file, boolean replace, InputStream data, long size, Optional<Instant> lastModified, ProgressListener progressListener) {
146148
Path filePath = resolve(file);
147149
var options = replace
148150
? EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
@@ -151,10 +153,12 @@ public CompletionStage<CloudItemMetadata> write(CloudPath file, boolean replace,
151153
Lock l = lock.writeLock();
152154
l.lock();
153155
try (var ch = FileChannel.open(filePath, options)) {
154-
var tmpSize = ch.transferFrom(Channels.newChannel(data), 0, Long.MAX_VALUE);
155-
var modifiedDate = Files.getLastModifiedTime(filePath).toInstant();
156-
var metadata = new CloudItemMetadata(file.getFileName().toString(), file, CloudItemType.FILE, Optional.of(modifiedDate), Optional.of(tmpSize));
157-
return CompletableFuture.completedFuture(metadata);
156+
var written = ch.transferFrom(Channels.newChannel(data), 0, Long.MAX_VALUE);
157+
assert size == written : "Written bytes should be equal to provided size";
158+
if(lastModified.isPresent()) {
159+
Files.setLastModifiedTime(filePath, FileTime.from(lastModified.get()));
160+
}
161+
return CompletableFuture.completedFuture(null);
158162
} catch (NoSuchFileException e) {
159163
return CompletableFuture.failedFuture(new NotFoundException(e));
160164
} catch (FileAlreadyExistsException e) {

src/main/java/org/cryptomator/cloudaccess/vaultformat8/VaultFormat8ProviderDecorator.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.nio.ByteBuffer;
2727
import java.nio.channels.Channels;
2828
import java.nio.charset.StandardCharsets;
29+
import java.time.Instant;
2930
import java.util.Optional;
3031
import java.util.UUID;
3132
import java.util.concurrent.CompletableFuture;
@@ -136,15 +137,15 @@ private long checkedAdd(long a, long b, long onOverflow) {
136137
}
137138

138139
@Override
139-
public CompletionStage<CloudItemMetadata> write(CloudPath file, boolean replace, InputStream data, long size, ProgressListener progressListener) {
140+
public CompletionStage<Void> write(CloudPath file, boolean replace, InputStream data, long size, Optional<Instant> lastModified, ProgressListener progressListener) {
140141
return getC9rPath(file).thenCompose(ciphertextPath -> {
141142
fileHeaderCache.evict(ciphertextPath);
142143
var src = Channels.newChannel(data);
143144
var encryptingChannel = new EncryptingReadableByteChannel(src, cryptor);
144145
var encryptedIn = Channels.newInputStream(encryptingChannel);
145146
long numBytes = Cryptors.ciphertextSize(size, cryptor) + cryptor.fileHeaderCryptor().headerSize();
146-
return delegate.write(ciphertextPath, replace, encryptedIn, numBytes, progressListener);
147-
}).thenApply(ciphertextMetadata -> toCleartextMetadata(ciphertextMetadata, file.getParent(), file.getFileName().toString()));
147+
return delegate.write(ciphertextPath, replace, encryptedIn, numBytes, lastModified, progressListener);
148+
});
148149
}
149150

150151
@Override
@@ -154,7 +155,7 @@ public CompletionStage<CloudPath> createFolder(CloudPath folder) {
154155

155156
var futureC9rFile = getC9rPath(folder)
156157
.thenCompose(delegate::createFolder)
157-
.thenCompose(folderPath -> delegate.write(folderPath.resolve(DIR_FILE_NAME), false, new ByteArrayInputStream(dirId), dirId.length, ProgressListener.NO_PROGRESS_AWARE));
158+
.thenCompose(folderPath -> delegate.write(folderPath.resolve(DIR_FILE_NAME), false, new ByteArrayInputStream(dirId), dirId.length, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE));
158159

159160
var futureDir = delegate.createFolderIfNonExisting(dirPath.getParent())
160161
.thenCompose(unused -> delegate.createFolder(dirPath));

src/main/java/org/cryptomator/cloudaccess/webdav/WebDavClient.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
import java.net.HttpURLConnection;
2222
import java.net.MalformedURLException;
2323
import java.net.URL;
24+
import java.time.Instant;
2425
import java.util.ArrayList;
2526
import java.util.Comparator;
2627
import java.util.List;
28+
import java.util.Optional;
2729
import java.util.stream.IntStream;
2830

2931
public class WebDavClient {
@@ -181,7 +183,7 @@ private InputStream read(final Request.Builder getRequest, final ProgressListene
181183
}
182184
}
183185

184-
CloudItemMetadata write(final CloudPath file, final boolean replace, final InputStream data, final long size, final ProgressListener progressListener) throws CloudProviderException {
186+
void write(final CloudPath file, final boolean replace, final InputStream data, final long size, final Optional<Instant> lastModified, final ProgressListener progressListener) throws CloudProviderException {
185187
if (!replace && exists(file)) {
186188
throw new AlreadyExistsException("CloudNode already exists and replace is false");
187189
}
@@ -191,9 +193,10 @@ CloudItemMetadata write(final CloudPath file, final boolean replace, final Input
191193
.url(absoluteURLFrom(file)) //
192194
.put(countingBody);
193195

196+
lastModified.ifPresent(instant -> requestBuilder.addHeader("X-OC-Mtime", String.valueOf(instant.getEpochSecond())));
197+
194198
try (final var response = httpClient.execute(requestBuilder)) {
195199
checkExecutionSucceeded(response.code());
196-
return itemMetadata(file);
197200
} catch (IOException e) {
198201
throw new CloudProviderException(e);
199202
}

src/main/java/org/cryptomator/cloudaccess/webdav/WebDavCloudProvider.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@
77
import org.cryptomator.cloudaccess.api.ProgressListener;
88

99
import java.io.InputStream;
10+
import java.time.Instant;
1011
import java.util.Optional;
1112
import java.util.concurrent.CompletableFuture;
1213
import java.util.concurrent.CompletionStage;
1314

14-
import static java.util.concurrent.CompletableFuture.supplyAsync;
15-
1615
public class WebDavCloudProvider implements CloudProvider {
1716

1817
private final WebDavClient webDavClient;
@@ -27,32 +26,32 @@ public static WebDavCloudProvider from(final WebDavCredential webDavCredential)
2726

2827
@Override
2928
public CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node) {
30-
return supplyAsync(() -> webDavClient.itemMetadata(node));
29+
return CompletableFuture.supplyAsync(() -> webDavClient.itemMetadata(node));
3130
}
3231

3332
@Override
3433
public CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pageToken) {
35-
return supplyAsync(() -> webDavClient.list(folder));
34+
return CompletableFuture.supplyAsync(() -> webDavClient.list(folder));
3635
}
3736

3837
@Override
3938
public CompletionStage<InputStream> read(CloudPath file, ProgressListener progressListener) {
40-
return supplyAsync(() -> webDavClient.read(file, progressListener));
39+
return CompletableFuture.supplyAsync(() -> webDavClient.read(file, progressListener));
4140
}
4241

4342
@Override
4443
public CompletionStage<InputStream> read(CloudPath file, long offset, long count, ProgressListener progressListener) {
45-
return supplyAsync(() -> webDavClient.read(file, offset, count, progressListener));
44+
return CompletableFuture.supplyAsync(() -> webDavClient.read(file, offset, count, progressListener));
4645
}
4746

4847
@Override
49-
public CompletionStage<CloudItemMetadata> write(CloudPath file, boolean replace, InputStream data, long size, ProgressListener progressListener) {
50-
return supplyAsync(() -> webDavClient.write(file, replace, data, size, progressListener));
48+
public CompletionStage<Void> write(CloudPath file, boolean replace, InputStream data, long size, Optional<Instant> lastModified, ProgressListener progressListener) {
49+
return CompletableFuture.runAsync(() -> webDavClient.write(file, replace, data, size, lastModified, progressListener));
5150
}
5251

5352
@Override
5453
public CompletionStage<CloudPath> createFolder(CloudPath folder) {
55-
return supplyAsync(() -> webDavClient.createFolder(folder));
54+
return CompletableFuture.supplyAsync(() -> webDavClient.createFolder(folder));
5655
}
5756

5857
@Override
@@ -62,7 +61,7 @@ public CompletionStage<Void> delete(CloudPath node) {
6261

6362
@Override
6463
public CompletionStage<CloudPath> move(CloudPath source, CloudPath target, boolean replace) {
65-
return supplyAsync(() -> webDavClient.move(source, target, replace));
64+
return CompletableFuture.supplyAsync(() -> webDavClient.move(source, target, replace));
6665
}
6766

6867
}

src/test/java/org/cryptomator/cloudaccess/MetadataCachingProviderDecoratorTest.java

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -263,30 +263,24 @@ public void testDeleteFolder() {
263263
public void testWriteToFile() {
264264
var updatedFile1Metadata = new CloudItemMetadata(file1Metadata.getName(), file1Metadata.getPath(), CloudItemType.FILE, Optional.of(Instant.EPOCH), Optional.of(15l));
265265

266-
Mockito.when(cloudProvider.write(Mockito.eq(file1Metadata.getPath()), Mockito.eq(false), Mockito.any(InputStream.class), Mockito.eq(15l), Mockito.eq(ProgressListener.NO_PROGRESS_AWARE)))
267-
.thenReturn(CompletableFuture.completedFuture(updatedFile1Metadata));
266+
Mockito.when(cloudProvider.write(Mockito.eq(file1Metadata.getPath()), Mockito.eq(false), Mockito.any(InputStream.class), Mockito.eq(15l), Mockito.eq(Optional.empty()), Mockito.eq(ProgressListener.NO_PROGRESS_AWARE)))
267+
.thenReturn(CompletableFuture.completedFuture(null));
268268

269-
var futureResult = decorator.write(file1Metadata.getPath(), false, new ByteArrayInputStream("TOPSECRET!".getBytes(UTF_8)),15l, ProgressListener.NO_PROGRESS_AWARE);
270-
var result = Assertions.assertTimeoutPreemptively(Duration.ofMillis(100), () -> futureResult.toCompletableFuture().get());
271-
272-
Assertions.assertEquals(file1Metadata.getPath(), result.getPath());
273-
Assertions.assertEquals(file1Metadata.getName(), result.getName());
274-
Assertions.assertEquals(CloudItemType.FILE, result.getItemType());
275-
Assertions.assertEquals(Optional.of(15l), result.getSize());
276-
Assertions.assertEquals(Optional.of(Instant.EPOCH), result.getLastModifiedDate());
269+
var futureResult = decorator.write(file1Metadata.getPath(), false, new ByteArrayInputStream("TOPSECRET!".getBytes(UTF_8)),15l, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE);
270+
Assertions.assertTimeoutPreemptively(Duration.ofMillis(100), () -> futureResult.toCompletableFuture().get());
277271

278-
Assertions.assertEquals(decorator.metadataCache.getIfPresent(file1Metadata.getPath()).get(), updatedFile1Metadata);
272+
Assertions.assertEquals(decorator.metadataCache.size(), 0l);
279273
}
280274

281275
@Test
282276
@DisplayName("write(\"/File 1\", replace=false, text, NO_PROGRESS_AWARE) throws NotFoundException")
283277
public void testWriteToFileNotFound() {
284278
decorator.metadataCache.put(file1Metadata.getPath(), Optional.of(file1Metadata));
285279

286-
Mockito.when(cloudProvider.write(Mockito.eq(file1Metadata.getPath()), Mockito.eq(false), Mockito.any(InputStream.class), Mockito.eq(15l), Mockito.eq(ProgressListener.NO_PROGRESS_AWARE)))
280+
Mockito.when(cloudProvider.write(Mockito.eq(file1Metadata.getPath()), Mockito.eq(false), Mockito.any(InputStream.class), Mockito.eq(15l), Mockito.eq(Optional.empty()), Mockito.eq(ProgressListener.NO_PROGRESS_AWARE)))
287281
.thenReturn(CompletableFuture.failedFuture(new NotFoundException()));
288282

289-
Assertions.assertThrows(NotFoundException.class, () -> decorator.write(file1Metadata.getPath(), false, new ByteArrayInputStream("TOPSECRET!".getBytes(UTF_8)),15l, ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join());
283+
Assertions.assertThrows(NotFoundException.class, () -> decorator.write(file1Metadata.getPath(), false, new ByteArrayInputStream("TOPSECRET!".getBytes(UTF_8)),15l, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE).toCompletableFuture().join());
290284

291285
Assertions.assertNull(decorator.metadataCache.getIfPresent(file1Metadata.getPath()));
292286
}

src/test/java/org/cryptomator/cloudaccess/localfs/LocalFsCloudProviderTest.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.nio.file.Files;
2020
import java.nio.file.Path;
2121
import java.time.Duration;
22+
import java.time.Instant;
23+
import java.util.Optional;
2224
import java.util.concurrent.ExecutionException;
2325
import java.util.stream.Collectors;
2426

@@ -92,10 +94,11 @@ public void testRandomAccessRead() throws IOException {
9294
public void testWriteToNewFile() throws IOException {
9395
var in = new ByteArrayInputStream("hallo welt".getBytes());
9496

95-
var result = provider.write(CloudPath.of("/file"), false, in, 10, ProgressListener.NO_PROGRESS_AWARE);
96-
var metaData = Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> result.toCompletableFuture().get());
97+
var result = provider.write(CloudPath.of("/file"), false, in, 10, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE);
98+
Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> result.toCompletableFuture().get());
99+
100+
var metaData = provider.itemMetadata(CloudPath.of("/file")).toCompletableFuture().join();
97101

98-
Assertions.assertEquals("file", metaData.getName());
99102
Assertions.assertEquals(CloudPath.of("/file"), metaData.getPath());
100103
Assertions.assertEquals(CloudItemType.FILE, metaData.getItemType());
101104
Assertions.assertTrue(metaData.getSize().isPresent());
@@ -110,7 +113,7 @@ public void testWriteToExistingFile() throws IOException, ExecutionException, In
110113
Files.write(root.resolve("file"), "hello world".getBytes());
111114
var in = new ByteArrayInputStream("hallo welt".getBytes());
112115

113-
var result = provider.write(CloudPath.of("/file"), false, in, 10, ProgressListener.NO_PROGRESS_AWARE);
116+
var result = provider.write(CloudPath.of("/file"), false, in, 10, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE);
114117

115118
Assertions.assertThrows(AlreadyExistsException.class, () -> {
116119
Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> result.toCompletableFuture().join());
@@ -123,8 +126,10 @@ public void testWriteToAndReplaceExistingFile() throws IOException {
123126
Files.write(root.resolve("file"), "hello world".getBytes());
124127
var in = new ByteArrayInputStream("hallo welt".getBytes());
125128

126-
var result = provider.write(CloudPath.of("/file"), true, in, 10, ProgressListener.NO_PROGRESS_AWARE);
127-
var metaData = Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> result.toCompletableFuture().get());
129+
var result = provider.write(CloudPath.of("/file"), true, in, 10, Optional.empty(), ProgressListener.NO_PROGRESS_AWARE);
130+
Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> result.toCompletableFuture().get());
131+
132+
var metaData = provider.itemMetadata(CloudPath.of("/file")).toCompletableFuture().join();
128133

129134
Assertions.assertEquals("file", metaData.getName());
130135
Assertions.assertEquals(CloudPath.of("/file"), metaData.getPath());
@@ -135,6 +140,28 @@ public void testWriteToAndReplaceExistingFile() throws IOException {
135140
Assertions.assertEquals(Files.getLastModifiedTime(root.resolve("file")).toInstant(), metaData.getLastModifiedDate().get());
136141
}
137142

143+
144+
@Test
145+
@DisplayName("write to /file (non-existing) update modification date")
146+
public void testWriteToNewFileUpdateModificationDate() throws IOException {
147+
var in = new ByteArrayInputStream("hallo welt".getBytes());
148+
149+
var modDate = Instant.now().minus(Duration.ofDays(365));
150+
151+
var result = provider.write(CloudPath.of("/file"), false, in, 10, Optional.of(modDate), ProgressListener.NO_PROGRESS_AWARE);
152+
Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> result.toCompletableFuture().get());
153+
154+
var metaData = provider.itemMetadata(CloudPath.of("/file")).toCompletableFuture().join();
155+
156+
Assertions.assertEquals("file", metaData.getName());
157+
Assertions.assertEquals(CloudPath.of("/file"), metaData.getPath());
158+
Assertions.assertEquals(CloudItemType.FILE, metaData.getItemType());
159+
Assertions.assertTrue(metaData.getSize().isPresent());
160+
Assertions.assertEquals(10, metaData.getSize().get());
161+
Assertions.assertTrue(metaData.getLastModifiedDate().isPresent());
162+
Assertions.assertEquals(Files.getLastModifiedTime(root.resolve("file")).toInstant(), modDate);
163+
}
164+
138165
@Test
139166
@DisplayName("create /folder")
140167
public void testCreateFolder() {

0 commit comments

Comments
 (0)