Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<guava.version>31.0.1-jre</guava.version>
<caffeine.version>3.0.5</caffeine.version>
<jwt.version>3.18.2</jwt.version>
<cryptolib.version>2.0.2</cryptolib.version>

Expand Down Expand Up @@ -70,6 +71,11 @@
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
exports org.cryptomator.cloudaccess;
exports org.cryptomator.cloudaccess.api;
exports org.cryptomator.cloudaccess.api.exceptions;
exports org.cryptomator.cloudaccess.requestdecorator;

requires java.xml;
requires com.google.common;
Expand All @@ -11,4 +12,5 @@
requires okhttp.digest;
requires okio;
requires com.auth0.jwt;
requires com.github.benmanes.caffeine;
}
10 changes: 7 additions & 3 deletions src/main/java/org/cryptomator/cloudaccess/CloudAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.cryptomator.cloudaccess.api.exceptions.VaultVerificationFailedException;
import org.cryptomator.cloudaccess.api.exceptions.VaultVersionVerificationFailedException;
import org.cryptomator.cloudaccess.localfs.LocalFsCloudProvider;
import org.cryptomator.cloudaccess.requestdecorator.CloudProviderDecoratorFactory;
import org.cryptomator.cloudaccess.vaultformat8.VaultFormat8ProviderDecorator;
import org.cryptomator.cloudaccess.webdav.WebDavCloudProvider;
import org.cryptomator.cloudaccess.webdav.WebDavCredential;
Expand Down Expand Up @@ -59,9 +60,12 @@ public static CloudProvider vaultFormat8GCMCloudAccess(CloudProvider cloudProvid

verifyVaultFormat8GCMConfig(cloudProvider, pathToVault, rawKey);

VaultFormat8ProviderDecorator provider = new VaultFormat8ProviderDecorator(cloudProvider, pathToVault.resolve("d"), cryptor);
provider.initialize();
return new MetadataCachingProviderDecorator(provider);
var decoratedCloudProvider = new CloudProviderDecoratorFactory().get(cloudProvider, cloudProvider.cachingCapability());

VaultFormat8ProviderDecorator vaultFormat8Provider = new VaultFormat8ProviderDecorator(decoratedCloudProvider, pathToVault.resolve("d"), cryptor);
vaultFormat8Provider.initialize();

return vaultFormat8Provider;
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("JVM doesn't supply a CSPRNG", e);
} catch (InterruptedException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.cryptomator.cloudaccess.requestdecorator;

import org.cryptomator.cloudaccess.api.CloudItemList;
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
import org.cryptomator.cloudaccess.api.CloudPath;
import org.cryptomator.cloudaccess.api.CloudProvider;
import org.cryptomator.cloudaccess.api.ProgressListener;
import org.cryptomator.cloudaccess.api.Quota;

import java.io.InputStream;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.CompletionStage;

abstract class CloudProviderDecorator implements CloudProvider {
Comment thread
overheadhunter marked this conversation as resolved.
Outdated

private final CloudProvider delegate;

public CloudProviderDecorator(CloudProvider delegate) {
this.delegate = delegate;
}

@Override
public CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node) {
return delegate.itemMetadata(node);
}

@Override
public CompletionStage<Quota> quota(CloudPath folder) {
return delegate.quota(folder);
}

@Override
public CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pageToken) {
return delegate.list(folder, pageToken);
}

@Override
public CompletionStage<InputStream> read(CloudPath file, long offset, long count, ProgressListener progressListener) {
return delegate.read(file, offset, count, progressListener);
}

@Override
public CompletionStage<Void> write(CloudPath file, boolean replace, InputStream data, long size, Optional<Instant> lastModified, ProgressListener progressListener) {
return delegate.write(file, replace, data, size, lastModified, progressListener);
}

@Override
public CompletionStage<CloudPath> createFolder(CloudPath folder) {
return delegate.createFolder(folder);
}

@Override
public CompletionStage<Void> deleteFile(CloudPath file) {
return delegate.deleteFile(file);
}

@Override
public CompletionStage<Void> deleteFolder(CloudPath folder) {
return delegate.deleteFolder(folder);
}

@Override
public CompletionStage<CloudPath> move(CloudPath source, CloudPath target, boolean replace) {
return delegate.move(source, target, replace);
}

@Override
public boolean cachingCapability() {
return delegate.cachingCapability();
}

@Override
public CompletionStage<Void> pollRemoteChanges() {
return delegate.pollRemoteChanges();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.cryptomator.cloudaccess.requestdecorator;

import org.cryptomator.cloudaccess.api.CloudProvider;

/**
* Factory class to add a caching or request-deduplication decorator around an existing {@link CloudProvider}.
*/
public class CloudProviderDecoratorFactory {

public CloudProvider get(CloudProvider cloudProvider, boolean cloudCachingCapability) {
if (cloudCachingCapability) {
var quotaCachingDecorator = new QuotaRequestCachingDecorator(cloudProvider);
return new MetadataRequestDeduplicationDecorator(quotaCachingDecorator);
} else {
return new MetadataCachingProviderDecorator(cloudProvider);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.cryptomator.cloudaccess;
package org.cryptomator.cloudaccess.requestdecorator;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
Expand All @@ -20,7 +20,7 @@
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;

public class MetadataCachingProviderDecorator implements CloudProvider {
class MetadataCachingProviderDecorator implements CloudProvider {

private final static int DEFAULT_CACHE_TIMEOUT_SECONDS = 10;

Expand Down Expand Up @@ -93,7 +93,7 @@ public CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pa
evictFromItemAndItemListCacheIncludingDescendants(folder);
} else if (delegate.cachingCapability()) {
evictFromItemListCache(entry);
} else if (exception == null) {
} else if (!delegate.cachingCapability() && exception == null) {
evictFromItemAndItemListCacheIncludingDescendants(folder);
assert cloudItemList != null;
cloudItemList.getItems().forEach(metadata -> cachedItemMetadataRequests.put(metadata.getPath(), CompletableFuture.completedFuture(metadata)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.cryptomator.cloudaccess.requestdecorator;

import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.cryptomator.cloudaccess.api.CloudItemList;
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
import org.cryptomator.cloudaccess.api.CloudPath;
import org.cryptomator.cloudaccess.api.CloudProvider;

import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletionStage;

/**
* Decorates an existing CloudProvider by deduplicating identical itemMetadata and list-requests so that the delegate is called only once until the future is completed.
*/
class MetadataRequestDeduplicationDecorator extends CloudProviderDecorator {


// visible for testing
final AsyncCache<CloudPath, CloudItemMetadata> cachedItemMetadataRequests;
final AsyncCache<ItemListEntry, CloudItemList> cachedItemListRequests;

private final CloudProvider delegate;

public MetadataRequestDeduplicationDecorator(CloudProvider delegate) {
this(delegate, //
Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(0)).buildAsync(), //
Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(0)).buildAsync());
Comment thread
overheadhunter marked this conversation as resolved.
}

MetadataRequestDeduplicationDecorator(
CloudProvider delegate, //
AsyncCache<CloudPath, CloudItemMetadata> cachedItemMetadataRequests, //
AsyncCache<ItemListEntry, CloudItemList> cachedItemListRequests) {
super(delegate);
this.delegate = delegate;
this.cachedItemMetadataRequests = cachedItemMetadataRequests;
this.cachedItemListRequests = cachedItemListRequests;
}

@Override
public CompletionStage<CloudItemMetadata> itemMetadata(CloudPath node) {
return cachedItemMetadataRequests.get(node, k -> delegate.itemMetadata(k).toCompletableFuture().join());
}

@Override
public CompletionStage<CloudItemList> list(CloudPath folder, Optional<String> pageToken) {
var entry = new ItemListEntry(folder, pageToken);
return cachedItemListRequests.get(entry, k -> delegate.list(k.path, k.pageToken).toCompletableFuture().join());
}

record ItemListEntry(CloudPath path, Optional<String> pageToken) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.cryptomator.cloudaccess.requestdecorator;

import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.cryptomator.cloudaccess.api.CloudPath;
import org.cryptomator.cloudaccess.api.CloudProvider;
import org.cryptomator.cloudaccess.api.Quota;
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
import org.cryptomator.cloudaccess.api.exceptions.QuotaNotAvailableException;

import java.time.Duration;
import java.util.concurrent.CompletionStage;

/**
* Decorates an existing CloudProvider by caching quota-requests for a duration of default 10 seconds (can be set using <code>org.cryptomator.cloudaccess.metadatacachingprovider.timeoutSeconds</code>).
*/
class QuotaRequestCachingDecorator extends CloudProviderDecorator {

private final static int DEFAULT_CACHE_TIMEOUT_SECONDS = 10;

// visible for testing
final AsyncCache<CloudPath, Quota> quotaCache;

private final CloudProvider delegate;

public QuotaRequestCachingDecorator(CloudProvider delegate) {
this(delegate, Caffeine
.newBuilder()
.expireAfterWrite(Duration.ofSeconds(Integer.getInteger("org.cryptomator.cloudaccess.metadatacachingprovider.timeoutSeconds", DEFAULT_CACHE_TIMEOUT_SECONDS)))
Comment thread
overheadhunter marked this conversation as resolved.
.buildAsync());
}

QuotaRequestCachingDecorator(CloudProvider delegate, AsyncCache<CloudPath, Quota> quotaCache) {
super(delegate);
this.delegate = delegate;
this.quotaCache = quotaCache;
}

@Override
public CompletionStage<Quota> quota(CloudPath folder) {
return quotaCache.get(folder, k -> delegate.quota(k).whenComplete((metadata, throwable) -> {
if (throwable != null && !(throwable instanceof NotFoundException) && !(throwable instanceof QuotaNotAvailableException)) {
quotaCache.synchronous().invalidate(folder);
}
}).toCompletableFuture().join());
Comment thread
SailReal marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.cryptomator.cloudaccess;
package org.cryptomator.cloudaccess.requestdecorator;

import org.cryptomator.cloudaccess.api.CloudItemList;
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
Expand All @@ -8,6 +8,7 @@
import org.cryptomator.cloudaccess.api.ProgressListener;
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
import org.cryptomator.cloudaccess.requestdecorator.MetadataCachingProviderDecorator;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.cryptomator.cloudaccess;
package org.cryptomator.cloudaccess.requestdecorator;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
Expand All @@ -10,6 +10,7 @@
import org.cryptomator.cloudaccess.api.ProgressListener;
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
import org.cryptomator.cloudaccess.requestdecorator.MetadataCachingProviderDecorator;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
Expand Down