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

Commit 2b5186b

Browse files
committed
Merge branch 'feature/webdav-caching' into develop
2 parents 5a9c7bc + 493dc67 commit 2b5186b

File tree

6 files changed

+529
-18
lines changed

6 files changed

+529
-18
lines changed

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
import java.nio.file.Path;
1414
import java.security.NoSuchAlgorithmException;
1515
import java.security.SecureRandom;
16-
import java.util.stream.Collectors;
17-
import java.util.stream.IntStream;
1816

1917
public class CloudAccess {
2018

@@ -37,7 +35,7 @@ public static CloudProvider vaultFormat8GCMCloudAccess(CloudProvider cloudProvid
3735
// TODO validate vaultFormat.jwt before creating decorator
3836
VaultFormat8ProviderDecorator provider = new VaultFormat8ProviderDecorator(cloudProvider, pathToVault.resolve("d"), cryptor);
3937
provider.initialize();
40-
return provider;
38+
return new MetadataCachingProviderDecorator(provider);
4139
} catch (NoSuchAlgorithmException e) {
4240
throw new IllegalStateException("JVM doesn't supply a CSPRNG", e);
4341
} catch (InterruptedException e) {
@@ -56,7 +54,8 @@ public static CloudProvider vaultFormat8GCMCloudAccess(CloudProvider cloudProvid
5654
*/
5755
public static CloudProvider toWebDAV(URL url, String username, CharSequence password) {
5856
// TODO can we pass though CharSequence to the auth mechanism?
59-
return WebDavCloudProvider.from(WebDavCredential.from(url, username, password.toString()));
57+
var webdavCloudProvider = WebDavCloudProvider.from(WebDavCredential.from(url, username, password.toString()));
58+
return new MetadataCachingProviderDecorator(webdavCloudProvider);
6059
}
6160

6261
/**
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package org.cryptomator.cloudaccess;
2+
3+
import com.google.common.cache.Cache;
4+
import com.google.common.cache.CacheBuilder;
5+
import org.cryptomator.cloudaccess.api.CloudItemList;
6+
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
7+
import org.cryptomator.cloudaccess.api.CloudPath;
8+
import org.cryptomator.cloudaccess.api.CloudProvider;
9+
import org.cryptomator.cloudaccess.api.ProgressListener;
10+
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
11+
12+
import java.io.InputStream;
13+
import java.time.Duration;
14+
import java.util.Optional;
15+
import java.util.concurrent.CompletableFuture;
16+
import java.util.concurrent.CompletionStage;
17+
import java.util.function.Function;
18+
19+
public class MetadataCachingProviderDecorator implements CloudProvider {
20+
21+
private final CloudProvider delegate;
22+
final Cache<CloudPath, Optional<CloudItemMetadata>> metadataCache;
23+
24+
public MetadataCachingProviderDecorator(CloudProvider delegate) {
25+
this(delegate, Duration.ofSeconds(10));
26+
}
27+
28+
public MetadataCachingProviderDecorator(CloudProvider delegate, Duration cacheEntryMaxAge) {
29+
this.delegate = delegate;
30+
this.metadataCache = CacheBuilder.newBuilder().expireAfterWrite(cacheEntryMaxAge).build();
31+
}
32+
33+
@Override
34+
public CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node) {
35+
var cachedMetadata = metadataCache.getIfPresent(node);
36+
if (cachedMetadata != null) {
37+
return cachedMetadata
38+
.map(CompletableFuture::completedFuture)
39+
.orElseGet(() -> CompletableFuture.failedFuture(new NotFoundException()));
40+
} else {
41+
return delegate.itemMetadata(node)
42+
.handle((metadata, exception) -> {
43+
if (exception == null) {
44+
assert metadata != null;
45+
metadataCache.put(node, Optional.of(metadata));
46+
return CompletableFuture.completedFuture(metadata);
47+
} else if (exception instanceof NotFoundException) {
48+
metadataCache.put(node, Optional.empty());
49+
return CompletableFuture.<CloudItemMetadata>failedFuture(exception);
50+
} else {
51+
metadataCache.invalidate(node);
52+
return CompletableFuture.<CloudItemMetadata>failedFuture(exception);
53+
}
54+
}).thenCompose(Function.identity());
55+
}
56+
}
57+
58+
@Override
59+
public CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pageToken) {
60+
return delegate.list(folder, pageToken)
61+
.handle((cloudItemList, exception) -> {
62+
evictIncludingDescendants(folder);
63+
if (exception == null) {
64+
assert cloudItemList != null;
65+
cloudItemList.getItems().forEach(metadata -> metadataCache.put(metadata.getPath(), Optional.of(metadata)));
66+
return CompletableFuture.completedFuture(cloudItemList);
67+
} else {
68+
return CompletableFuture.<CloudItemList>failedFuture(exception);
69+
}
70+
}).thenCompose(Function.identity());
71+
}
72+
73+
@Override
74+
public CompletionStage<InputStream> read(CloudPath file, ProgressListener progressListener) {
75+
return delegate.read(file, progressListener)
76+
.handle((metadata, exception) -> {
77+
if (exception == null) {
78+
assert metadata != null;
79+
return CompletableFuture.completedFuture(metadata);
80+
} else {
81+
metadataCache.invalidate(file);
82+
return CompletableFuture.<InputStream>failedFuture(exception);
83+
}
84+
}).thenCompose(Function.identity());
85+
}
86+
87+
@Override
88+
public CompletionStage<InputStream> read(CloudPath file, long offset, long count, ProgressListener progressListener) {
89+
return delegate.read(file, offset, count, progressListener)
90+
.handle((inputStream, exception) -> {
91+
if (exception == null) {
92+
assert inputStream != null;
93+
return CompletableFuture.completedFuture(inputStream);
94+
} else {
95+
metadataCache.invalidate(file);
96+
return CompletableFuture.<InputStream>failedFuture(exception);
97+
}
98+
}).thenCompose(Function.identity());
99+
}
100+
101+
@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());
115+
}
116+
117+
@Override
118+
public CompletionStage<CloudPath> createFolder(CloudPath folder) {
119+
return delegate.createFolder(folder)
120+
.handle((metadata, exception) -> {
121+
metadataCache.invalidate(folder);
122+
if (exception == null) {
123+
assert metadata != null;
124+
return CompletableFuture.completedFuture(metadata);
125+
} else {
126+
return CompletableFuture.<CloudPath>failedFuture(exception);
127+
}
128+
}).thenCompose(Function.identity());
129+
}
130+
131+
@Override
132+
public CompletionStage<Void> delete(CloudPath node) {
133+
return delegate.delete(node)
134+
.handle((nullReturn, exception) -> {
135+
evictIncludingDescendants(node);
136+
if (exception == null) {
137+
return CompletableFuture.completedFuture(nullReturn);
138+
} else {
139+
return CompletableFuture.<Void>failedFuture(exception);
140+
}
141+
}).thenCompose(Function.identity());
142+
}
143+
144+
@Override
145+
public CompletionStage<CloudPath> move(CloudPath source, CloudPath target, boolean replace) {
146+
return delegate.move(source, target, replace)
147+
.handle((path, exception) -> {
148+
metadataCache.invalidate(source);
149+
metadataCache.invalidate(target);
150+
if (exception == null) {
151+
return CompletableFuture.completedFuture(path);
152+
} else {
153+
return CompletableFuture.<CloudPath>failedFuture(exception);
154+
}
155+
}).thenCompose(Function.identity());
156+
}
157+
158+
private void evictIncludingDescendants(CloudPath cleartextPath) {
159+
for(var path : metadataCache.asMap().keySet()) {
160+
if(path.startsWith(cleartextPath)) {
161+
metadataCache.invalidate(path);
162+
}
163+
}
164+
}
165+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727

2828
public class WebDavClient {
2929

30+
private static final Comparator<PropfindEntryData> ASCENDING_BY_DEPTH = Comparator.comparingInt(PropfindEntryData::getDepth);
31+
3032
private final WebDavCompatibleHttpClient httpClient;
3133
private final URL baseUrl;
3234
private final int HTTP_INSUFFICIENT_STORAGE = 507;
33-
private final Comparator<PropfindEntryData> ASCENDING_BY_DEPTH = Comparator.comparingInt(PropfindEntryData::getDepth);
3435

3536
WebDavClient(final WebDavCompatibleHttpClient httpClient, final WebDavCredential webDavCredential) {
3637
this.httpClient = httpClient;

0 commit comments

Comments
 (0)