Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazily load pre-signed URLs #14221

Merged
merged 10 commits into from Feb 14, 2023
90 changes: 53 additions & 37 deletions azure/src/main/java/ch/cyberduck/core/azure/AzureUrlProvider.java
Expand Up @@ -27,16 +27,13 @@
import ch.cyberduck.core.Scheme;
import ch.cyberduck.core.URIEncoder;
import ch.cyberduck.core.UserDateFormatterFactory;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.PromptUrlProvider;
import ch.cyberduck.core.preferences.HostPreferences;

import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumSet;
import java.util.TimeZone;

Expand Down Expand Up @@ -66,57 +63,76 @@ public boolean isSupported(final Path file, final Type type) {
}

@Override
public DescriptiveUrl toDownloadUrl(final Path file, final Void options, final PasswordCallback callback) throws BackgroundException {
public DescriptiveUrl toDownloadUrl(final Path file, final Void options, final PasswordCallback callback) {
return this.createSignedUrl(file, new HostPreferences(session.getHost()).getInteger("s3.url.expire.seconds"));
}

@Override
public DescriptiveUrl toUploadUrl(final Path file, final Void options, final PasswordCallback callback) throws BackgroundException {
public DescriptiveUrl toUploadUrl(final Path file, final Void options, final PasswordCallback callback) {
return DescriptiveUrl.EMPTY;
}

private DescriptiveUrl createSignedUrl(final Path file, int seconds) throws BackgroundException {
try {
private DescriptiveUrl createSignedUrl(final Path file, int seconds) {
return new SharedAccessSignatureUrl(file, this.getExpiry(seconds));
}

protected Calendar getExpiry(final int seconds) {
final Calendar expiry = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
expiry.add(Calendar.SECOND, seconds);
return expiry;
}

private final class SharedAccessSignatureUrl extends DescriptiveUrl {
private final Path file;
private final Calendar expiry;

public SharedAccessSignatureUrl(final Path file, final Calendar expiry) {
super(EMPTY);
this.file = file;
this.expiry = expiry;
}

@Override
public String getUrl() {
final CloudBlob blob;
try {
if(!session.isConnected()) {
return DescriptiveUrl.EMPTY;
return DescriptiveUrl.EMPTY.getUrl();
}
blob = session.getClient().getContainerReference(containerService.getContainer(file).getName())
.getBlobReferenceFromServer(containerService.getKey(file));
.getBlobReferenceFromServer(containerService.getKey(file));
final String token;
token = blob.generateSharedAccessSignature(this.getPolicy(expiry), null);
return String.format("%s://%s%s?%s",
Scheme.https.name(), session.getHost().getHostname(), URIEncoder.encode(file.getAbsolute()), token);
}
catch(URISyntaxException e) {
return DescriptiveUrl.EMPTY;
}
final String token;
try {
token = blob.generateSharedAccessSignature(this.getPolicy(seconds), null);
catch(InvalidKeyException | URISyntaxException | StorageException e) {
return DescriptiveUrl.EMPTY.getUrl();
}
catch(InvalidKeyException e) {
return DescriptiveUrl.EMPTY;
}
return new DescriptiveUrl(URI.create(String.format("%s://%s%s?%s",
Scheme.https.name(), session.getHost().getHostname(), URIEncoder.encode(file.getAbsolute()), token)),
DescriptiveUrl.Type.signed,
MessageFormat.format(LocaleFactory.localizedString("{0} URL"), LocaleFactory.localizedString("Pre-Signed", "S3"))
+ " (" + MessageFormat.format(LocaleFactory.localizedString("Expires {0}", "S3") + ")",
UserDateFormatterFactory.get().getShortFormat(this.getExpiry(seconds))));
}
catch(StorageException e) {
throw new AzureExceptionMappingService().map(e);

private SharedAccessBlobPolicy getPolicy(final Calendar expiry) {
final SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy();
policy.setSharedAccessExpiryTime(expiry.getTime());
policy.setPermissions(EnumSet.of(SharedAccessBlobPermissions.READ));
return policy;
}
}

private SharedAccessBlobPolicy getPolicy(final int expiry) {
final SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy();
policy.setSharedAccessExpiryTime(new Date(this.getExpiry(expiry)));
policy.setPermissions(EnumSet.of(SharedAccessBlobPermissions.READ));
return policy;
}
@Override
public String getPreview() {
return MessageFormat.format(LocaleFactory.localizedString("Expires {0}", "S3") + ")",
UserDateFormatterFactory.get().getMediumFormat(expiry.getTimeInMillis()));
}

protected Long getExpiry(final int seconds) {
final Calendar expiry = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
expiry.add(Calendar.SECOND, seconds);
return expiry.getTimeInMillis();
@Override
public Type getType() {
return DescriptiveUrl.Type.signed;
}

@Override
public String getHelp() {
return MessageFormat.format(LocaleFactory.localizedString("{0} URL"),
LocaleFactory.localizedString("Pre-Signed", "S3"));
}
}
}
Expand Up @@ -42,6 +42,6 @@ public void testDisconnected() throws Exception {
final AzureUrlProvider provider = new AzureUrlProvider(session);
final Path container = new Path("test.cyberduck.ch", EnumSet.of(Path.Type.directory, Path.Type.volume));
final Path file = new Path(container, new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file));
assertEquals(DescriptiveUrl.EMPTY, provider.toDownloadUrl(file, null, new DisabledPasswordCallback()));
assertEquals(DescriptiveUrl.EMPTY.getUrl(), provider.toDownloadUrl(file, null, new DisabledPasswordCallback()).getUrl());
}
}
14 changes: 10 additions & 4 deletions core/src/main/java/ch/cyberduck/core/DescriptiveUrl.java
Expand Up @@ -71,9 +71,6 @@ public DescriptiveUrl(final DescriptiveUrl other) {
}

public String getUrl() {
if(null == url) {
return null;
}
return url.toString();
}

Expand All @@ -85,6 +82,10 @@ public String getHelp() {
return help;
}

public String getPreview() {
return this.getUrl();
}

@Override
public boolean equals(final Object o) {
if(this == o) {
Expand All @@ -104,6 +105,11 @@ public int hashCode() {

@Override
public String toString() {
return this.getUrl();
final StringBuilder sb = new StringBuilder("DescriptiveUrl{");
sb.append("url=").append(url);
sb.append(", type=").append(type);
sb.append(", help='").append(help).append('\'');
sb.append('}');
return sb.toString();
}
}
Expand Up @@ -14,7 +14,7 @@ public void testEquals() {
assertEquals(new DescriptiveUrl(URI.create("http://host.domain"), DescriptiveUrl.Type.provider, "a"), new DescriptiveUrl(URI.create("http://host.domain"), DescriptiveUrl.Type.provider, "b"));
assertNotEquals(new DescriptiveUrl(URI.create("http://host.domainb"), DescriptiveUrl.Type.provider, "a"), new DescriptiveUrl(URI.create("http://host.domain"), DescriptiveUrl.Type.provider, "b"));
assertEquals(new DescriptiveUrl(URI.create("http://host.domain"), DescriptiveUrl.Type.http), new DescriptiveUrl(URI.create("http://host.domain"), DescriptiveUrl.Type.http));
assertEquals("http://host.domain", new DescriptiveUrl(URI.create("http://host.domain"), DescriptiveUrl.Type.http).toString());
assertEquals("http://host.domain", new DescriptiveUrl(URI.create("http://host.domain"), DescriptiveUrl.Type.http).getUrl());
}

@Test
Expand Down
Expand Up @@ -190,7 +190,7 @@ public Distribution read(final Path container, final Distribution.Method method,
};
}
if(type == UrlProvider.class) {
return (T) new SwiftUrlProvider(this, accounts, regionService, distributions);
return (T) new SwiftUrlProvider(this, accounts, distributions);
}
if(type == Find.class) {
return (T) new SwiftFindFeature(this);
Expand Down