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

Commit 1477a59

Browse files
committed
Merge branch 'release/1.1.0' into main
2 parents 85a1e60 + 3bd8b85 commit 1477a59

File tree

13 files changed

+214
-134
lines changed

13 files changed

+214
-134
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<groupId>org.cryptomator</groupId>
77
<artifactId>cloud-access</artifactId>
8-
<version>1.0.0</version>
8+
<version>1.1.0</version>
99

1010
<name>Cryptomator CloudAccess in Java</name>
1111
<description>CloudAccess is used in e.g. Cryptomator for Android to access different cloud providers.</description>

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

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,33 @@
44
import com.google.common.cache.CacheBuilder;
55
import org.cryptomator.cloudaccess.api.CloudItemList;
66
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
7+
import org.cryptomator.cloudaccess.api.CloudItemType;
78
import org.cryptomator.cloudaccess.api.CloudPath;
89
import org.cryptomator.cloudaccess.api.CloudProvider;
910
import org.cryptomator.cloudaccess.api.ProgressListener;
1011
import org.cryptomator.cloudaccess.api.Quota;
1112
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
13+
import org.cryptomator.cloudaccess.api.exceptions.QuotaNotAvailableException;
1214

1315
import java.io.InputStream;
1416
import java.time.Duration;
1517
import java.time.Instant;
1618
import java.util.Optional;
1719
import java.util.concurrent.CompletableFuture;
1820
import java.util.concurrent.CompletionStage;
21+
import java.util.concurrent.ExecutionException;
1922

2023
public class MetadataCachingProviderDecorator implements CloudProvider {
2124

2225
private final static int DEFAULT_CACHE_TIMEOUT_SECONDS = 10;
2326

24-
final Cache<CloudPath, Optional<CloudItemMetadata>> itemMetadataCache;
25-
final Cache<CloudPath, Optional<Quota>> quotaCache;
27+
final Cache<CloudPath, CompletionStage<CloudItemMetadata>> itemMetadataCache;
28+
final Cache<CloudPath, CompletionStage<Quota>> quotaCache;
2629
private final CloudProvider delegate;
2730

2831
public MetadataCachingProviderDecorator(CloudProvider delegate) {
2932
this(delegate, Duration.ofSeconds( //
30-
Integer.getInteger("org.cryptomator.cloudaccess.metadatacachingprovider.timeoutSeconds", DEFAULT_CACHE_TIMEOUT_SECONDS)
33+
Integer.getInteger("org.cryptomator.cloudaccess.metadatacachingprovider.timeoutSeconds", DEFAULT_CACHE_TIMEOUT_SECONDS) //
3134
));
3235
}
3336

@@ -39,45 +42,29 @@ public MetadataCachingProviderDecorator(CloudProvider delegate, Duration cacheEn
3942

4043
@Override
4144
public CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node) {
42-
var cachedMetadata = itemMetadataCache.getIfPresent(node);
43-
if (cachedMetadata != null) {
44-
return cachedMetadata //
45-
.map(CompletableFuture::completedFuture) //
46-
.orElseGet(() -> CompletableFuture.failedFuture(new NotFoundException()));
47-
} else {
48-
return delegate.itemMetadata(node) //
49-
.whenComplete((metadata, exception) -> {
50-
if (exception == null) {
51-
assert metadata != null;
52-
itemMetadataCache.put(node, Optional.of(metadata));
53-
} else if (exception instanceof NotFoundException) {
54-
itemMetadataCache.put(node, Optional.empty());
55-
} else {
56-
itemMetadataCache.invalidate(node);
57-
}
58-
});
45+
try {
46+
return itemMetadataCache.get(node, () -> delegate.itemMetadata(node).whenComplete((metadata, throwable) -> {
47+
// immediately invalidate cache in case of exceptions (except for NOT FOUND):
48+
if (throwable != null && !(throwable instanceof NotFoundException)) {
49+
itemMetadataCache.invalidate(node);
50+
}
51+
}));
52+
} catch (ExecutionException e) {
53+
return CompletableFuture.failedFuture(e);
5954
}
6055
}
6156

6257
@Override
6358
public CompletionStage<Quota> quota(CloudPath folder) {
64-
var cachedMetadata = quotaCache.getIfPresent(folder);
65-
if (cachedMetadata != null) {
66-
return cachedMetadata //
67-
.map(CompletableFuture::completedFuture) //
68-
.orElseGet(() -> CompletableFuture.failedFuture(new NotFoundException()));
69-
} else {
70-
return delegate.quota(folder) //
71-
.whenComplete((quota, exception) -> {
72-
if (exception == null) {
73-
assert quota != null;
74-
quotaCache.put(folder, Optional.of(quota));
75-
} else if (exception instanceof NotFoundException) {
76-
quotaCache.put(folder, Optional.empty());
77-
} else {
78-
quotaCache.invalidate(folder);
79-
}
80-
});
59+
try {
60+
return quotaCache.get(folder, () -> delegate.quota(folder).whenComplete((metadata, throwable) -> {
61+
// immediately invalidate cache in case of exceptions (except for NOT FOUND and QUOTA NOT AVAILABLE):
62+
if (throwable != null && !(throwable instanceof NotFoundException) && !(throwable instanceof QuotaNotAvailableException)) {
63+
quotaCache.invalidate(folder);
64+
}
65+
}));
66+
} catch (ExecutionException e) {
67+
return CompletableFuture.failedFuture(e);
8168
}
8269
}
8370

@@ -88,7 +75,7 @@ public CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pa
8875
evictIncludingDescendants(folder);
8976
if (exception == null) {
9077
assert cloudItemList != null;
91-
cloudItemList.getItems().forEach(metadata -> itemMetadataCache.put(metadata.getPath(), Optional.of(metadata)));
78+
cloudItemList.getItems().forEach(metadata -> itemMetadataCache.put(metadata.getPath(), CompletableFuture.completedFuture(metadata)));
9279
}
9380
});
9481
}
@@ -117,9 +104,11 @@ public CompletionStage<InputStream> read(CloudPath file, long offset, long count
117104
public CompletionStage<Void> write(CloudPath file, boolean replace, InputStream data, long size, Optional<Instant> lastModified, ProgressListener progressListener) {
118105
return delegate.write(file, replace, data, size, lastModified, progressListener) //
119106
.whenComplete((nullReturn, exception) -> {
120-
if (exception != null) {
121-
itemMetadataCache.invalidate(file);
107+
if (exception == null) {
108+
itemMetadataCache.put(file, CompletableFuture.completedFuture(new CloudItemMetadata(file.getFileName().toString(), file, CloudItemType.FILE, lastModified, Optional.of(size))));
122109
quotaCache.invalidateAll();
110+
} else {
111+
itemMetadataCache.invalidate(file);
123112
}
124113
});
125114
}
@@ -129,14 +118,27 @@ public CompletionStage<CloudPath> createFolder(CloudPath folder) {
129118
return delegate.createFolder(folder) //
130119
.whenComplete((metadata, exception) -> {
131120
itemMetadataCache.invalidate(folder);
121+
quotaCache.invalidateAll();
122+
});
123+
}
124+
125+
@Override
126+
public CompletionStage<Void> deleteFile(CloudPath file) {
127+
return delegate.deleteFile(file) //
128+
.whenComplete((nullReturn, exception) -> {
129+
CompletionStage<CloudItemMetadata> future = CompletableFuture.failedFuture(new NotFoundException());
130+
itemMetadataCache.put(file, future);
131+
quotaCache.invalidateAll();
132132
});
133133
}
134134

135135
@Override
136-
public CompletionStage<Void> delete(CloudPath node) {
137-
return delegate.delete(node) //
136+
public CompletionStage<Void> deleteFolder(CloudPath folder) {
137+
return delegate.deleteFolder(folder) //
138138
.whenComplete((nullReturn, exception) -> {
139-
evictIncludingDescendants(node);
139+
evictIncludingDescendants(folder);
140+
CompletionStage<CloudItemMetadata> future = CompletableFuture.failedFuture(new NotFoundException());
141+
itemMetadataCache.put(folder, future);
140142
quotaCache.invalidateAll();
141143
});
142144
}
@@ -145,8 +147,8 @@ public CompletionStage<Void> delete(CloudPath node) {
145147
public CompletionStage<CloudPath> move(CloudPath source, CloudPath target, boolean replace) {
146148
return delegate.move(source, target, replace) //
147149
.whenComplete((path, exception) -> {
148-
itemMetadataCache.invalidate(source);
149-
itemMetadataCache.invalidate(target);
150+
evictIncludingDescendants(source);
151+
evictIncludingDescendants(target);
150152
quotaCache.invalidateAll();
151153
});
152154
}

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,32 @@ default CompletionStage<CloudPath> createFolderIfNonExisting(CloudPath folder) {
181181
}
182182

183183
/**
184-
* Recursively delete a file or folder.
184+
* Delete a file.
185185
* <p>
186186
* The returned CompletionStage might complete exceptionally with one of the following exceptions:
187187
* <ul>
188188
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException} If no item exists for the given path</li>
189189
* <li>{@link CloudProviderException} in case of generic I/O errors</li>
190190
* </ul>
191191
*
192-
* @param node The remote path of the file or folder to delete.
193-
* @return CompletionStage completing successfully if node was deleted.
192+
* @param file The remote path of the file to delete.
193+
* @return CompletionStage completing successfully if file was deleted.
194194
*/
195-
CompletionStage<Void> delete(CloudPath node);
195+
CompletionStage<Void> deleteFile(CloudPath file);
196+
197+
/**
198+
* Recursively delete a folder.
199+
* <p>
200+
* The returned CompletionStage might complete exceptionally with one of the following exceptions:
201+
* <ul>
202+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException} If no item exists for the given path</li>
203+
* <li>{@link CloudProviderException} in case of generic I/O errors</li>
204+
* </ul>
205+
*
206+
* @param folder The remote path of the folder to delete.
207+
* @return CompletionStage completing successfully if folder was deleted.
208+
*/
209+
CompletionStage<Void> deleteFolder(CloudPath folder);
196210

197211
/**
198212
* Move a file or folder to a different location.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.cryptomator.cloudaccess.api.exceptions;
2+
3+
public class CloudTimeoutException extends CloudProviderException {
4+
5+
public CloudTimeoutException(Throwable cause) {
6+
super(cause);
7+
}
8+
9+
}

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,25 @@ public CompletionStage<CloudPath> createFolder(CloudPath folder) {
195195
}
196196

197197
@Override
198-
public CompletionStage<Void> delete(CloudPath node) {
199-
Path path = resolve(node);
198+
public CompletionStage<Void> deleteFile(CloudPath file) {
199+
Path path = resolve(file);
200+
Lock l = lock.writeLock();
201+
l.lock();
202+
try {
203+
Files.delete(path);
204+
return CompletableFuture.completedFuture(null);
205+
} catch (NoSuchFileException e) {
206+
return CompletableFuture.failedFuture(new NotFoundException(e));
207+
} catch (IOException e) {
208+
return CompletableFuture.failedFuture(new CloudProviderException(e));
209+
} finally {
210+
l.unlock();
211+
}
212+
}
213+
214+
@Override
215+
public CompletionStage<Void> deleteFolder(CloudPath folder) {
216+
Path path = resolve(folder);
200217
Lock l = lock.writeLock();
201218
l.lock();
202219
try {

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -177,20 +177,20 @@ public CompletionStage<CloudPath> createFolder(CloudPath folder) {
177177
}
178178

179179
@Override
180-
public CompletionStage<Void> delete(CloudPath node) {
181-
return itemMetadata(node).thenCompose(cloudNode -> {
182-
if (cloudNode.getItemType() == CloudItemType.FILE) {
183-
return getC9rPath(node).thenCompose(ciphertextPath -> {
180+
public CompletionStage<Void> deleteFile(CloudPath file) {
181+
return getC9rPath(file) //
182+
.thenCompose(ciphertextPath -> {
184183
fileHeaderCache.evict(ciphertextPath);
185-
return delegate.delete(ciphertextPath);
184+
return delegate.deleteFile(ciphertextPath);
186185
});
187-
} else {
188-
return deleteCiphertextDir(getDirPathFromClearTextDir(node)) //
189-
.thenCompose(ignored -> getC9rPath(node)) //
190-
.thenCompose(delegate::delete) //
191-
.thenRun(() -> dirIdCache.evictIncludingDescendants(node));
192-
}
193-
});
186+
}
187+
188+
@Override
189+
public CompletionStage<Void> deleteFolder(CloudPath folder) {
190+
return deleteCiphertextDir(getDirPathFromClearTextDir(folder)) //
191+
.thenCompose(ignored -> getC9rPath(folder)) //
192+
.thenCompose(delegate::deleteFolder) //
193+
.thenRun(() -> dirIdCache.evictIncludingDescendants(folder));
194194
}
195195

196196
private CompletionStage<Void> deleteCiphertextDir(CompletionStage<CloudPath> dirPath) {
@@ -203,7 +203,7 @@ private CompletionStage<Void> deleteCiphertextDir(CompletionStage<CloudPath> dir
203203
var futures = result.map(CompletionStage::toCompletableFuture).toArray(CompletableFuture[]::new);
204204
return CompletableFuture.allOf(futures);
205205
}).thenCombine(dirPath, (unused, path) -> path) //
206-
.thenCompose(delegate::delete);
206+
.thenCompose(delegate::deleteFolder);
207207
}
208208

209209
@Override

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.cryptomator.cloudaccess.api.Quota;
1313
import org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException;
1414
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
15+
import org.cryptomator.cloudaccess.api.exceptions.CloudTimeoutException;
1516
import org.cryptomator.cloudaccess.api.exceptions.InsufficientStorageException;
1617
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
1718
import org.cryptomator.cloudaccess.api.exceptions.ParentFolderDoesNotExistException;
@@ -23,6 +24,7 @@
2324
import java.io.ByteArrayInputStream;
2425
import java.io.IOException;
2526
import java.io.InputStream;
27+
import java.io.InterruptedIOException;
2628
import java.net.HttpURLConnection;
2729
import java.net.MalformedURLException;
2830
import java.net.URL;
@@ -53,6 +55,8 @@ CloudItemMetadata itemMetadata(final CloudPath path) throws CloudProviderExcepti
5355
checkPropfindExecutionSucceeded(response.code());
5456

5557
return processGet(getEntriesFromResponse(response), path);
58+
} catch (InterruptedIOException e) {
59+
throw new CloudTimeoutException(e);
5660
} catch (IOException | SAXException e) {
5761
throw new CloudProviderException(e);
5862
}
@@ -80,6 +84,8 @@ Quota quota(final CloudPath folder) throws CloudProviderException {
8084
try (final var responseBody = response.body()) {
8185
return new PropfindResponseParser().parseQuta(responseBody.byteStream());
8286
}
87+
} catch (InterruptedIOException e) {
88+
throw new CloudTimeoutException(e);
8389
} catch (IOException | SAXException e) {
8490
throw new CloudProviderException(e);
8591
}
@@ -93,6 +99,8 @@ CloudItemList list(final CloudPath folder) throws CloudProviderException {
9399
final var nodes = getEntriesFromResponse(response);
94100

95101
return processDirList(nodes, folder);
102+
} catch (InterruptedIOException e) {
103+
throw new CloudTimeoutException(e);
96104
} catch (IOException | SAXException e) {
97105
throw new CloudProviderException(e);
98106
}
@@ -202,6 +210,8 @@ CloudPath move(final CloudPath from, final CloudPath to, boolean replace) throws
202210
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
203211
}
204212
}
213+
} catch (InterruptedIOException e) {
214+
throw new CloudTimeoutException(e);
205215
} catch (IOException e) {
206216
throw new CloudProviderException(e);
207217
}
@@ -247,6 +257,8 @@ private InputStream read(final Request.Builder getRequest, final ProgressListene
247257
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
248258
}
249259
}
260+
} catch (InterruptedIOException e) {
261+
throw new CloudTimeoutException(e);
250262
} catch (IOException e) {
251263
throw new CloudProviderException(e);
252264
} finally {
@@ -278,14 +290,17 @@ void write(final CloudPath file, final boolean replace, final InputStream data,
278290
throw new ForbiddenException();
279291
case HttpURLConnection.HTTP_BAD_METHOD:
280292
throw new TypeMismatchException();
281-
case HttpURLConnection.HTTP_CONFLICT:
293+
case HttpURLConnection.HTTP_CONFLICT: // fall through
294+
case HttpURLConnection.HTTP_NOT_FOUND: // necessary due to a bug in Nextcloud, see https://github.com/nextcloud/server/issues/23519
282295
throw new ParentFolderDoesNotExistException();
283296
case HTTP_INSUFFICIENT_STORAGE:
284297
throw new InsufficientStorageException();
285298
default:
286299
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
287300
}
288301
}
302+
} catch (InterruptedIOException e) {
303+
throw new CloudTimeoutException(e);
289304
} catch (IOException e) {
290305
throw new CloudProviderException(e);
291306
}
@@ -324,6 +339,8 @@ CloudPath createFolder(final CloudPath path) throws CloudProviderException {
324339
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
325340
}
326341
}
342+
} catch (InterruptedIOException e) {
343+
throw new CloudTimeoutException(e);
327344
} catch (IOException e) {
328345
throw new CloudProviderException(e);
329346
}
@@ -348,6 +365,8 @@ void delete(final CloudPath path) throws CloudProviderException {
348365
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
349366
}
350367
}
368+
} catch (InterruptedIOException e) {
369+
throw new CloudTimeoutException(e);
351370
} catch (IOException e) {
352371
throw new CloudProviderException(e);
353372
}

0 commit comments

Comments
 (0)