diff --git a/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index 04ad43f25057..f354072f9fd3 100644
--- a/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
+++ b/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
@@ -2119,6 +2119,70 @@ public static Builder newBuilder() {
*/
URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options);
+ /**
+ * Generates a signed URL for a blob. If you have a blob that you want to allow access to for a
+ * fixed amount of time, you can use this method to generate a URL that is only valid within a
+ * certain time period. This is particularly useful if you don't want publicly accessible blobs,
+ * but also don't want to require users to explicitly log in. Signing a URL requires
+ * a service account signer. If an instance of {@link com.google.auth.ServiceAccountSigner} was
+ * passed to {@link StorageOptions}' builder via {@code setCredentials(Credentials)} or the
+ * default credentials are being used and the environment variable
+ * {@code GOOGLE_APPLICATION_CREDENTIALS} is set or your application is running in App Engine,
+ * then {@code signUrl} will use that credentials to sign the URL. If the credentials passed to
+ * {@link StorageOptions} do not implement {@link ServiceAccountSigner} (this is the case, for
+ * instance, for Google Cloud SDK credentials) then {@code signUrl} will throw an
+ * {@link IllegalStateException} unless an implementation of {@link ServiceAccountSigner} is
+ * passed using the {@link SignUrlOption#signWith(ServiceAccountSigner)} option.
+ *
+ *
A service account signer is looked for in the following order:
+ *
+ * - The signer passed with the option {@link SignUrlOption#signWith(ServiceAccountSigner)}
+ *
- The credentials passed to {@link StorageOptions}
+ *
- The default credentials, if no credentials were passed to {@link StorageOptions}
+ *
+ *
+ * Example of creating a signed URL that is valid for 2 weeks, using the default credentials
+ * for signing the URL.
+ *
{@code
+ * String bucketName = "my_unique_bucket";
+ * String blobName = "my_blob_name";
+ * URL signedUrl = storage.signUrl(BlobInfo.newBuilder(bucketName, blobName).build(), 14,
+ * TimeUnit.DAYS);
+ * }
+ *
+ * Example of creating a signed URL passing the
+ * {@link SignUrlOption#signWith(ServiceAccountSigner)} option, that will be used for signing the
+ * URL.
+ *
{@code
+ * String bucketName = "my_unique_bucket";
+ * String blobName = "my_blob_name";
+ * String keyPath = "/path/to/key.json";
+ * URL signedUrl = storage.signUrl(BlobInfo.newBuilder(bucketName, blobName).build(),
+ * 14, TimeUnit.DAYS, SignUrlOption.signWith(
+ * ServiceAccountCredentials.fromStream(new FileInputStream(keyPath))));
+ * }
+ *
+ * Note that the {@link ServiceAccountSigner} may require additional configuration to enable
+ * URL signing. See the documentation for the implementation for more details.
+ *
+ * @param url can be customize
+ * @param blobInfo the blob associated with the signed URL
+ * @param duration time until the signed URL expires, expressed in {@code unit}. The finest
+ * granularity supported is 1 second, finer granularities will be truncated
+ * @param unit time unit of the {@code duration} parameter
+ * @param options optional URL signing options
+ * @throws IllegalStateException if {@link SignUrlOption#signWith(ServiceAccountSigner)} was not
+ * used and no implementation of {@link ServiceAccountSigner} was provided to
+ * {@link StorageOptions}
+ * @throws IllegalArgumentException if {@code SignUrlOption.withMd5()} option is used and
+ * {@code blobInfo.md5()} is {@code null}
+ * @throws IllegalArgumentException if {@code SignUrlOption.withContentType()} option is used and
+ * {@code blobInfo.contentType()} is {@code null}
+ * @throws SigningException if the attempt to sign the URL failed
+ * @see Signed-URLs
+ */
+ URL signUrl(String url, BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options);
+
/**
* Gets the requested blobs. A batch request is used to perform this call.
*
diff --git a/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
index 787388006571..9d3809de23c8 100644
--- a/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
+++ b/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
@@ -84,6 +84,7 @@ final class StorageImpl extends BaseService implements Storage {
private static final String EMPTY_BYTE_ARRAY_MD5 = "1B2M2Y8AsgTpgAmY7PhCfg==";
private static final String EMPTY_BYTE_ARRAY_CRC32C = "AAAAAA==";
private static final String PATH_DELIMITER = "/";
+ private static final String DEFAULT_STORAGE_HOST = "https://storage.googleapis.com";
private static final Function, Boolean> DELETE_FUNCTION =
new Function, Boolean>() {
@@ -499,54 +500,63 @@ private BlobWriteChannel writer(BlobInfo blobInfo, BlobTargetOption... options)
@Override
public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options) {
- EnumMap optionMap = Maps.newEnumMap(SignUrlOption.Option.class);
- for (SignUrlOption option : options) {
- optionMap.put(option.getOption(), option.getValue());
- }
- ServiceAccountSigner credentials =
- (ServiceAccountSigner) optionMap.get(SignUrlOption.Option.SERVICE_ACCOUNT_CRED);
- if (credentials == null) {
- checkState(this.getOptions().getCredentials() instanceof ServiceAccountSigner,
- "Signing key was not provided and could not be derived");
- credentials = (ServiceAccountSigner) this.getOptions().getCredentials();
- }
-
- long expiration = TimeUnit.SECONDS.convert(
- getOptions().getClock().millisTime() + unit.toMillis(duration), TimeUnit.MILLISECONDS);
-
- StringBuilder stPath = new StringBuilder();
- if (!blobInfo.getBucket().startsWith(PATH_DELIMITER)) {
- stPath.append(PATH_DELIMITER);
- }
- stPath.append(blobInfo.getBucket());
- if (!blobInfo.getBucket().endsWith(PATH_DELIMITER)) {
- stPath.append(PATH_DELIMITER);
- }
- if (blobInfo.getName().startsWith(PATH_DELIMITER)) {
- stPath.setLength(stPath.length() - 1);
- }
-
- String escapedName = UrlEscapers.urlFragmentEscaper().escape(blobInfo.getName());
- stPath.append(escapedName.replace("?", "%3F"));
-
- URI path = URI.create(stPath.toString());
-
- try {
- SignatureInfo signatureInfo = buildSignatureInfo(optionMap, blobInfo, expiration, path);
- byte[] signatureBytes =
- credentials.sign(signatureInfo.constructUnsignedPayload().getBytes(UTF_8));
- StringBuilder stBuilder = new StringBuilder("https://storage.googleapis.com").append(path);
- String signature =
- URLEncoder.encode(BaseEncoding.base64().encode(signatureBytes), UTF_8.name());
- stBuilder.append("?GoogleAccessId=").append(credentials.getAccount());
- stBuilder.append("&Expires=").append(expiration);
- stBuilder.append("&Signature=").append(signature);
-
- return new URL(stBuilder.toString());
-
- } catch (MalformedURLException | UnsupportedEncodingException ex) {
- throw new IllegalStateException(ex);
- }
+ return signUrlOptions(DEFAULT_STORAGE_HOST, blobInfo, duration, unit, options);
+ }
+
+ @Override
+ public URL signUrl(String url, BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options) {
+ return signUrlOptions(url, blobInfo, duration, unit, options);
+ }
+
+ private URL signUrlOptions(String url, BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options){
+ EnumMap optionMap = Maps.newEnumMap(SignUrlOption.Option.class);
+ for (SignUrlOption option : options) {
+ optionMap.put(option.getOption(), option.getValue());
+ }
+ ServiceAccountSigner credentials =
+ (ServiceAccountSigner) optionMap.get(SignUrlOption.Option.SERVICE_ACCOUNT_CRED);
+ if (credentials == null) {
+ checkState(this.getOptions().getCredentials() instanceof ServiceAccountSigner,
+ "Signing key was not provided and could not be derived");
+ credentials = (ServiceAccountSigner) this.getOptions().getCredentials();
+ }
+
+ long expiration = TimeUnit.SECONDS.convert(
+ getOptions().getClock().millisTime() + unit.toMillis(duration), TimeUnit.MILLISECONDS);
+
+ StringBuilder stPath = new StringBuilder();
+ if (!blobInfo.getBucket().startsWith(PATH_DELIMITER)) {
+ stPath.append(PATH_DELIMITER);
+ }
+ stPath.append(blobInfo.getBucket());
+ if (!blobInfo.getBucket().endsWith(PATH_DELIMITER)) {
+ stPath.append(PATH_DELIMITER);
+ }
+ if (blobInfo.getName().startsWith(PATH_DELIMITER)) {
+ stPath.setLength(stPath.length() - 1);
+ }
+
+ String escapedName = UrlEscapers.urlFragmentEscaper().escape(blobInfo.getName());
+ stPath.append(escapedName.replace("?", "%3F"));
+
+ URI path = URI.create(stPath.toString());
+
+ try {
+ SignatureInfo signatureInfo = buildSignatureInfo(optionMap, blobInfo, expiration, path);
+ byte[] signatureBytes =
+ credentials.sign(signatureInfo.constructUnsignedPayload().getBytes(UTF_8));
+ StringBuilder stBuilder = new StringBuilder(url).append(path);
+ String signature =
+ URLEncoder.encode(BaseEncoding.base64().encode(signatureBytes), UTF_8.name());
+ stBuilder.append("?GoogleAccessId=").append(credentials.getAccount());
+ stBuilder.append("&Expires=").append(expiration);
+ stBuilder.append("&Signature=").append(signature);
+
+ return new URL(stBuilder.toString());
+
+ } catch (MalformedURLException | UnsupportedEncodingException ex) {
+ throw new IllegalStateException(ex);
+ }
}
/**