Skip to content

Commit d60d3f4

Browse files
committed
Make signUrl take long duration and TimeUnit parameter
- Add TimeSource abstract class to wrap clock functionalities (waiting for Java8 Clock) - Add timeSource setter to StorageOptions builder and getter to StorageOptions - Update StorageImplTest and ITStorageTest - Update javadoc for Blob/Storage signUrl
1 parent cedc9a2 commit d60d3f4

7 files changed

Lines changed: 110 additions & 29 deletions

File tree

gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.Collections;
3333
import java.util.List;
3434
import java.util.Objects;
35+
import java.util.concurrent.TimeUnit;
3536

3637
/**
3738
* A Google cloud storage object.
@@ -246,13 +247,15 @@ public BlobWriteChannel writer(BlobTargetOption... options) {
246247
* time period. This is particularly useful if you don't want publicly accessible blobs, but don't
247248
* want to require users to explicitly log in.
248249
*
249-
* @param expirationTimeInSeconds the signed URL expiration (using epoch time)
250-
* @param options signed url options
250+
* @param duration time until the signed URL expires, expressed in {@code unit}. The finer
251+
* granularity supported is 1 second
252+
* @param unit time unit of the {@code duration} parameter
253+
* @param options optional URL signing options
251254
* @return a signed URL for this bucket and the specified options
252255
* @see <a href="https://cloud.google.com/storage/docs/access-control#Signed-URLs">Signed-URLs</a>
253256
*/
254-
public URL signUrl(long expirationTimeInSeconds, SignUrlOption... options) {
255-
return storage.signUrl(info, expirationTimeInSeconds, options);
257+
public URL signUrl(long duration, TimeUnit unit, SignUrlOption... options) {
258+
return storage.signUrl(info, duration, unit, options);
256259
}
257260

258261
/**

gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.LinkedList;
3535
import java.util.List;
3636
import java.util.Set;
37+
import java.util.concurrent.TimeUnit;
3738

3839
/**
3940
* An interface for Google Cloud Storage.
@@ -643,15 +644,17 @@ public static Builder builder() {
643644
* <p>
644645
* Example usage of creating a signed URL that is valid for 2 weeks:
645646
* <pre> {@code
646-
* service.signUrl(BlobInfo.of("bucket", "name"), TimeUnit.DAYS.toSeconds(14));
647+
* service.signUrl(BlobInfo.of("bucket", "name"), 14, TimeUnit.DAYS);
647648
* }</pre>
648649
*
649-
* @param blobInfo the blob associated with the signed url
650-
* @param expirationTimeInSeconds the signed URL expiration (using epoch time)
650+
* @param blobInfo the blob associated with the signed URL
651+
* @param duration time until the signed URL expires, expressed in {@code unit}. The finer
652+
* granularity supported is 1 second
653+
* @param unit time unit of the {@code duration} parameter
651654
* @param options optional URL signing options
652655
* @see <a href="https://cloud.google.com/storage/docs/access-control#Signed-URLs">Signed-URLs</a>
653656
*/
654-
URL signUrl(BlobInfo blobInfo, long expirationTimeInSeconds, SignUrlOption... options);
657+
URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options);
655658

656659
/**
657660
* Gets the requested blobs. A batch request is used to perform this call.

gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import java.util.Map;
6969
import java.util.Set;
7070
import java.util.concurrent.Callable;
71+
import java.util.concurrent.TimeUnit;
7172

7273
final class StorageImpl extends BaseService<StorageOptions> implements Storage {
7374

@@ -521,7 +522,9 @@ public BlobWriteChannel writer(BlobInfo blobInfo, BlobTargetOption... options) {
521522
}
522523

523524
@Override
524-
public URL signUrl(BlobInfo blobInfo, long expiration, SignUrlOption... options) {
525+
public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options) {
526+
long expiration = TimeUnit.SECONDS.convert(
527+
options().timeSource().millis() + unit.toMillis(duration), TimeUnit.MILLISECONDS);
525528
EnumMap<SignUrlOption.Option, Object> optionMap = Maps.newEnumMap(SignUrlOption.Option.class);
526529
for (SignUrlOption option : options) {
527530
optionMap.put(option.option(), option.value());

gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageOptions.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.gcloud.spi.StorageRpc;
2424
import com.google.gcloud.spi.StorageRpcFactory;
2525

26+
import java.io.Serializable;
2627
import java.util.Objects;
2728
import java.util.Set;
2829

@@ -34,24 +35,75 @@ public class StorageOptions extends ServiceOptions<StorageRpc, StorageOptions> {
3435
private static final String DEFAULT_PATH_DELIMITER = "/";
3536

3637
private final String pathDelimiter;
38+
private final TimeSource timeSource;
3739
private transient StorageRpc storageRpc;
3840

41+
/**
42+
* A class providing access to the current time in milliseconds.
43+
*
44+
* Implementations should implement {@code Serializable} wherever possible and must document
45+
* whether or not they do support serialization.
46+
*/
47+
public static abstract class TimeSource {
48+
49+
private static TimeSource DEFAULT_TIME_SOURCE = new DefaultTimeSource();
50+
51+
/**
52+
* Returns current time in milliseconds according to this time source.
53+
*/
54+
public abstract long millis();
55+
56+
/**
57+
* Returns the default time source.
58+
*/
59+
public static TimeSource defaultTimeSource() {
60+
return DEFAULT_TIME_SOURCE;
61+
}
62+
63+
private static class DefaultTimeSource extends TimeSource implements Serializable {
64+
65+
private static final long serialVersionUID = -5077300394286703864L;
66+
67+
@Override
68+
public long millis() {
69+
return System.currentTimeMillis();
70+
}
71+
}
72+
}
73+
3974
public static class Builder extends
4075
ServiceOptions.Builder<StorageRpc, StorageOptions, Builder> {
4176

4277
private String pathDelimiter;
78+
private TimeSource timeSource;
4379

4480
private Builder() {}
4581

4682
private Builder(StorageOptions options) {
4783
super(options);
4884
}
4985

86+
/**
87+
* Sets the path delimiter for the storage service.
88+
* @param pathDelimiter the path delimiter to set
89+
* @return the builder.
90+
*/
5091
public Builder pathDelimiter(String pathDelimiter) {
5192
this.pathDelimiter = pathDelimiter;
5293
return this;
5394
}
5495

96+
/**
97+
* Sets the time source for the storage service. The time source is used by `signUrl` to compute
98+
* URL's expiry time. If no time source is set by default `System.getTimeMillis()` is used.
99+
* @param source the time source to set
100+
* @return the builder.
101+
*/
102+
public Builder timeSource(TimeSource source) {
103+
this.timeSource = source;
104+
return this;
105+
}
106+
55107
@Override
56108
public StorageOptions build() {
57109
return new StorageOptions(this);
@@ -61,6 +113,7 @@ public StorageOptions build() {
61113
private StorageOptions(Builder builder) {
62114
super(builder);
63115
pathDelimiter = MoreObjects.firstNonNull(builder.pathDelimiter, DEFAULT_PATH_DELIMITER);
116+
timeSource = MoreObjects.firstNonNull(builder.timeSource, TimeSource.defaultTimeSource());
64117
// todo: consider providing read-timeout
65118
}
66119

@@ -84,10 +137,21 @@ StorageRpc storageRpc() {
84137
return storageRpc;
85138
}
86139

140+
/**
141+
* Returns the storage service's path delimiter.
142+
*/
87143
public String pathDelimiter() {
88144
return pathDelimiter;
89145
}
90146

147+
/**
148+
* Returns the storage service's time source. Default time source uses `System.getTimeMillis()` to
149+
* get current time.
150+
*/
151+
public TimeSource timeSource() {
152+
return timeSource;
153+
}
154+
91155
@Override
92156
public Builder toBuilder() {
93157
return new Builder(this);

gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.net.URL;
3939
import java.util.Arrays;
4040
import java.util.List;
41+
import java.util.concurrent.TimeUnit;
4142

4243
public class BlobTest {
4344

@@ -161,9 +162,9 @@ public void testWriter() throws Exception {
161162
@Test
162163
public void testSignUrl() throws Exception {
163164
URL url = new URL("http://localhost:123/bla");
164-
expect(storage.signUrl(BLOB_INFO, 100)).andReturn(url);
165+
expect(storage.signUrl(BLOB_INFO, 100, TimeUnit.SECONDS)).andReturn(url);
165166
replay(storage);
166-
assertEquals(url, blob.signUrl(100));
167+
assertEquals(url, blob.signUrl(100, TimeUnit.SECONDS));
167168
}
168169

169170
@Test

gcloud-java-storage/src/test/java/com/google/gcloud/storage/ITStorageTest.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -435,10 +435,7 @@ public void testGetSignedUrl() throws IOException {
435435
String blobName = "test-get-signed-url-blob";
436436
BlobInfo blob = BlobInfo.of(bucket, blobName);
437437
assertNotNull(storage.create(BlobInfo.of(bucket, blobName), BLOB_BYTE_CONTENT));
438-
Calendar calendar = Calendar.getInstance();
439-
calendar.add(Calendar.HOUR, 1);
440-
long expiration = calendar.getTimeInMillis() / 1000;
441-
URL url = storage.signUrl(blob, expiration);
438+
URL url = storage.signUrl(blob, 1, TimeUnit.HOURS);
442439
URLConnection connection = url.openConnection();
443440
byte[] readBytes = new byte[BLOB_BYTE_CONTENT.length];
444441
try (InputStream responseStream = connection.getInputStream()) {
@@ -453,10 +450,8 @@ public void testPostSignedUrl() throws IOException {
453450
String blobName = "test-post-signed-url-blob";
454451
BlobInfo blob = BlobInfo.of(bucket, blobName);
455452
assertNotNull(storage.create(BlobInfo.of(bucket, blobName)));
456-
Calendar calendar = Calendar.getInstance();
457-
calendar.add(Calendar.HOUR, 1);
458-
long expiration = calendar.getTimeInMillis() / 1000;
459-
URL url = storage.signUrl(blob, expiration, Storage.SignUrlOption.httpMethod(HttpMethod.POST));
453+
URL url =
454+
storage.signUrl(blob, 1, TimeUnit.HOURS, Storage.SignUrlOption.httpMethod(HttpMethod.POST));
460455
URLConnection connection = url.openConnection();
461456
connection.setDoOutput(true);
462457
connection.connect();

gcloud-java-storage/src/test/java/com/google/gcloud/storage/StorageImplTest.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import java.security.spec.X509EncodedKeySpec;
6666
import java.util.List;
6767
import java.util.Map;
68+
import java.util.concurrent.TimeUnit;
6869

6970
public class StorageImplTest {
7071

@@ -171,6 +172,13 @@ public class StorageImplTest {
171172
+ "EkPPhszldvQTY486uPxyD/D7HdfnGW/Nbw5JUhfvecAdudDEhNAQ3PNabyDMI+TpiHy4NTWOrgdcWrzj6VXcdc"
172173
+ "+uuABnPwRCdcyJ1xl2kOrPksRnp1auNGMLOe4IpEBjGY7baX9UG8+A45MbG0aHmkR59Op/aR9XowIDAQAB";
173174

175+
private static final StorageOptions.TimeSource TIME_SOURCE = new StorageOptions.TimeSource() {
176+
@Override
177+
public long millis() {
178+
return 42000L;
179+
}
180+
};
181+
174182
private static PrivateKey privateKey;
175183
private static PublicKey publicKey;
176184

@@ -794,24 +802,26 @@ public void testSignUrl() throws NoSuchAlgorithmException, InvalidKeyException,
794802
String account = "account";
795803
ServiceAccountAuthCredentials credentialsMock =
796804
EasyMock.createMock(ServiceAccountAuthCredentials.class);
797-
EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock).times(1);
805+
EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock);
798806
EasyMock.expect(optionsMock.authCredentials()).andReturn(credentialsMock).times(2);
807+
EasyMock.expect(optionsMock.timeSource()).andReturn(TIME_SOURCE);
799808
EasyMock.expect(credentialsMock.privateKey()).andReturn(privateKey);
800809
EasyMock.expect(credentialsMock.account()).andReturn(account);
801810
EasyMock.replay(optionsMock, storageRpcMock, credentialsMock);
802811
storage = StorageFactory.instance().get(optionsMock);
803-
URL url = storage.signUrl(BLOB_INFO1, 60);
812+
URL url = storage.signUrl(BLOB_INFO1, 14, TimeUnit.DAYS);
804813
String stringUrl = url.toString();
805814
String expectedUrl =
806815
new StringBuilder("https://storage.googleapis.com/").append(BUCKET_NAME1).append("/")
807816
.append(BLOB_NAME1).append("?GoogleAccessId=").append(account).append("&Expires=")
808-
.append(60).append("&Signature=").toString();
817+
.append(42L + 1209600).append("&Signature=").toString();
809818
assertTrue(stringUrl.startsWith(expectedUrl));
810819
String signature = stringUrl.substring(expectedUrl.length());
811820

812821
StringBuilder signedMessageBuilder = new StringBuilder();
813-
signedMessageBuilder.append(HttpMethod.GET).append('\n').append('\n').append('\n').append(60)
814-
.append('\n').append("/").append(BUCKET_NAME1).append("/").append(BLOB_NAME1);
822+
signedMessageBuilder.append(HttpMethod.GET).append('\n').append('\n').append('\n')
823+
.append(42L + 1209600).append('\n').append("/").append(BUCKET_NAME1).append("/")
824+
.append(BLOB_NAME1);
815825

816826
Signature signer = Signature.getInstance("SHA256withRSA");
817827
signer.initVerify(publicKey);
@@ -827,27 +837,29 @@ public void testSignUrlWithOptions() throws NoSuchAlgorithmException, InvalidKey
827837
String account = "account";
828838
ServiceAccountAuthCredentials credentialsMock =
829839
EasyMock.createMock(ServiceAccountAuthCredentials.class);
830-
EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock).times(1);
840+
EasyMock.expect(optionsMock.storageRpc()).andReturn(storageRpcMock);
831841
EasyMock.expect(optionsMock.authCredentials()).andReturn(credentialsMock).times(2);
842+
EasyMock.expect(optionsMock.timeSource()).andReturn(TIME_SOURCE);
832843
EasyMock.expect(credentialsMock.privateKey()).andReturn(privateKey);
833844
EasyMock.expect(credentialsMock.account()).andReturn(account);
834845
EasyMock.replay(optionsMock, storageRpcMock, credentialsMock);
835846
storage = StorageFactory.instance().get(optionsMock);
836847
URL url =
837-
storage.signUrl(BLOB_INFO1, 60, Storage.SignUrlOption.httpMethod(HttpMethod.POST),
848+
storage.signUrl(BLOB_INFO1, 14, TimeUnit.DAYS,
849+
Storage.SignUrlOption.httpMethod(HttpMethod.POST),
838850
Storage.SignUrlOption.withContentType(), Storage.SignUrlOption.withMd5());
839851
String stringUrl = url.toString();
840852
String expectedUrl =
841853
new StringBuilder("https://storage.googleapis.com/").append(BUCKET_NAME1).append("/")
842854
.append(BLOB_NAME1).append("?GoogleAccessId=").append(account).append("&Expires=")
843-
.append(60).append("&Signature=").toString();
855+
.append(42L + 1209600).append("&Signature=").toString();
844856
assertTrue(stringUrl.startsWith(expectedUrl));
845857
String signature = stringUrl.substring(expectedUrl.length());
846858

847859
StringBuilder signedMessageBuilder = new StringBuilder();
848860
signedMessageBuilder.append(HttpMethod.POST).append('\n').append(BLOB_INFO1.md5()).append('\n')
849-
.append(BLOB_INFO1.contentType()).append('\n').append(60).append('\n').append("/")
850-
.append(BUCKET_NAME1).append("/").append(BLOB_NAME1);
861+
.append(BLOB_INFO1.contentType()).append('\n').append(42L + 1209600).append('\n')
862+
.append("/").append(BUCKET_NAME1).append("/").append(BLOB_NAME1);
851863

852864
Signature signer = Signature.getInstance("SHA256withRSA");
853865
signer.initVerify(publicKey);

0 commit comments

Comments
 (0)