Skip to content

Commit 5595770

Browse files
committed
Merge pull request #271 from mziccard/add-blob-write-options
Add BlobWriteOption to support MD5 and CRC32C checks on create/write
2 parents 2665ab5 + 354d047 commit 5595770

File tree

5 files changed

+227
-36
lines changed

5 files changed

+227
-36
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.common.collect.Lists;
2525
import com.google.gcloud.spi.StorageRpc;
2626
import com.google.gcloud.storage.Storage.BlobTargetOption;
27+
import com.google.gcloud.storage.Storage.BlobWriteOption;
2728
import com.google.gcloud.storage.Storage.CopyRequest;
2829
import com.google.gcloud.storage.Storage.SignUrlOption;
2930

@@ -269,12 +270,14 @@ public BlobReadChannel reader(BlobSourceOption... options) {
269270
}
270271

271272
/**
272-
* Returns a {@code BlobWriteChannel} object for writing to this blob.
273+
* Returns a {@code BlobWriteChannel} object for writing to this blob. By default any md5 and
274+
* crc32c values in the current blob are ignored unless requested via the
275+
* {@code BlobWriteOption.md5Match} and {@code BlobWriteOption.crc32cMatch} options.
273276
*
274277
* @param options target blob options
275278
* @throws StorageException upon failure
276279
*/
277-
public BlobWriteChannel writer(BlobTargetOption... options) {
280+
public BlobWriteChannel writer(BlobWriteOption... options) {
278281
return storage.writer(info, options);
279282
}
280283

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

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121

2222
import com.google.common.collect.ImmutableList;
2323
import com.google.common.collect.Iterables;
24+
import com.google.common.collect.Lists;
2425
import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials;
2526
import com.google.gcloud.Service;
2627
import com.google.gcloud.spi.StorageRpc;
28+
import com.google.gcloud.spi.StorageRpc.Tuple;
2729

2830
import java.io.InputStream;
2931
import java.io.Serializable;
@@ -33,6 +35,7 @@
3335
import java.util.LinkedHashSet;
3436
import java.util.LinkedList;
3537
import java.util.List;
38+
import java.util.Objects;
3639
import java.util.Set;
3740
import java.util.concurrent.TimeUnit;
3841

@@ -145,6 +148,105 @@ public static BlobTargetOption metagenerationMatch() {
145148
public static BlobTargetOption metagenerationNotMatch() {
146149
return new BlobTargetOption(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH);
147150
}
151+
152+
static Tuple<BlobInfo, BlobTargetOption[]> convert(BlobInfo info, BlobWriteOption... options) {
153+
BlobInfo.Builder infoBuilder = info.toBuilder().crc32c(null).md5(null);
154+
List<BlobTargetOption> targetOptions = Lists.newArrayListWithCapacity(options.length);
155+
for (BlobWriteOption option : options) {
156+
switch (option.option) {
157+
case IF_CRC32C_MATCH:
158+
infoBuilder.crc32c(info.crc32c());
159+
break;
160+
case IF_MD5_MATCH:
161+
infoBuilder.md5(info.md5());
162+
break;
163+
default:
164+
targetOptions.add(option.toTargetOption());
165+
break;
166+
}
167+
}
168+
return Tuple.of(infoBuilder.build(),
169+
targetOptions.toArray(new BlobTargetOption[targetOptions.size()]));
170+
}
171+
}
172+
173+
class BlobWriteOption implements Serializable {
174+
175+
private static final long serialVersionUID = -3880421670966224580L;
176+
177+
private final Option option;
178+
private final Object value;
179+
180+
enum Option {
181+
PREDEFINED_ACL, IF_GENERATION_MATCH, IF_GENERATION_NOT_MATCH, IF_METAGENERATION_MATCH,
182+
IF_METAGENERATION_NOT_MATCH, IF_MD5_MATCH, IF_CRC32C_MATCH;
183+
184+
StorageRpc.Option toRpcOption() {
185+
return StorageRpc.Option.valueOf(this.name());
186+
}
187+
}
188+
189+
BlobTargetOption toTargetOption() {
190+
return new BlobTargetOption(this.option.toRpcOption(), this.value);
191+
}
192+
193+
private BlobWriteOption(Option option, Object value) {
194+
this.option = option;
195+
this.value = value;
196+
}
197+
198+
private BlobWriteOption(Option option) {
199+
this(option, null);
200+
}
201+
202+
@Override
203+
public int hashCode() {
204+
return Objects.hash(option, value);
205+
}
206+
207+
@Override
208+
public boolean equals(Object obj) {
209+
if (obj == null) {
210+
return false;
211+
}
212+
if (!(obj instanceof BlobWriteOption)) {
213+
return false;
214+
}
215+
final BlobWriteOption other = (BlobWriteOption) obj;
216+
return this.option == other.option && Objects.equals(this.value, other.value);
217+
}
218+
219+
public static BlobWriteOption predefinedAcl(PredefinedAcl acl) {
220+
return new BlobWriteOption(Option.PREDEFINED_ACL, acl.entry());
221+
}
222+
223+
public static BlobWriteOption doesNotExist() {
224+
return new BlobWriteOption(Option.IF_GENERATION_MATCH, 0L);
225+
}
226+
227+
public static BlobWriteOption generationMatch() {
228+
return new BlobWriteOption(Option.IF_GENERATION_MATCH);
229+
}
230+
231+
public static BlobWriteOption generationNotMatch() {
232+
return new BlobWriteOption(Option.IF_GENERATION_NOT_MATCH);
233+
}
234+
235+
public static BlobWriteOption metagenerationMatch() {
236+
return new BlobWriteOption(Option.IF_METAGENERATION_MATCH);
237+
}
238+
239+
public static BlobWriteOption metagenerationNotMatch() {
240+
return new BlobWriteOption(Option.IF_METAGENERATION_NOT_MATCH);
241+
}
242+
243+
public static BlobWriteOption md5Match() {
244+
return new BlobWriteOption(Option.IF_MD5_MATCH, true);
245+
}
246+
247+
public static BlobWriteOption crc32cMatch() {
248+
return new BlobWriteOption(Option.IF_CRC32C_MATCH, true);
249+
}
148250
}
149251

150252
class BlobSourceOption extends Option {
@@ -510,21 +612,25 @@ public static Builder builder() {
510612

511613
/**
512614
* Create a new blob. Direct upload is used to upload {@code content}. For large content,
513-
* {@link #writer} is recommended as it uses resumable upload.
615+
* {@link #writer} is recommended as it uses resumable upload. MD5 and CRC32C hashes of
616+
* {@code content} are computed and used for validating transferred data.
514617
*
515618
* @return a complete blob information.
516619
* @throws StorageException upon failure
620+
* @see <a href="https://cloud.google.com/storage/docs/hashes-etags">Hashes and ETags</a>
517621
*/
518622
BlobInfo create(BlobInfo blobInfo, byte[] content, BlobTargetOption... options);
519623

520624
/**
521625
* Create a new blob. Direct upload is used to upload {@code content}. For large content,
522-
* {@link #writer} is recommended as it uses resumable upload.
626+
* {@link #writer} is recommended as it uses resumable upload. By default any md5 and crc32c
627+
* values in the given {@code blobInfo} are ignored unless requested via the
628+
* {@code BlobWriteOption.md5Match} and {@code BlobWriteOption.crc32cMatch} options.
523629
*
524630
* @return a complete blob information.
525631
* @throws StorageException upon failure
526632
*/
527-
BlobInfo create(BlobInfo blobInfo, InputStream content, BlobTargetOption... options);
633+
BlobInfo create(BlobInfo blobInfo, InputStream content, BlobWriteOption... options);
528634

529635
/**
530636
* Return the requested bucket or {@code null} if not found.
@@ -679,11 +785,13 @@ public static Builder builder() {
679785
BlobReadChannel reader(BlobId blob, BlobSourceOption... options);
680786

681787
/**
682-
* Create a blob and return a channel for writing its content.
788+
* Create a blob and return a channel for writing its content. By default any md5 and crc32c
789+
* values in the given {@code blobInfo} are ignored unless requested via the
790+
* {@code BlobWriteOption.md5Match} and {@code BlobWriteOption.crc32cMatch} options.
683791
*
684792
* @throws StorageException upon failure
685793
*/
686-
BlobWriteChannel writer(BlobInfo blobInfo, BlobTargetOption... options);
794+
BlobWriteChannel writer(BlobInfo blobInfo, BlobWriteOption... options);
687795

688796
/**
689797
* Generates a signed URL for a blob.

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

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import com.google.common.collect.Lists;
4141
import com.google.common.collect.Maps;
4242
import com.google.common.collect.Sets;
43+
import com.google.common.hash.Hashing;
4344
import com.google.common.io.BaseEncoding;
4445
import com.google.common.primitives.Ints;
4546
import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials;
@@ -93,13 +94,14 @@ public RetryResult beforeEval(Exception exception) {
9394
static final ExceptionHandler EXCEPTION_HANDLER = ExceptionHandler.builder()
9495
.abortOn(RuntimeException.class).interceptor(EXCEPTION_HANDLER_INTERCEPTOR).build();
9596
private static final byte[] EMPTY_BYTE_ARRAY = {};
97+
private static final String EMPTY_BYTE_ARRAY_MD5 = "1B2M2Y8AsgTpgAmY7PhCfg==";
98+
private static final String EMPTY_BYTE_ARRAY_CRC32C = "AAAAAA==";
9699

97100
private final StorageRpc storageRpc;
98101

99102
StorageImpl(StorageOptions options) {
100103
super(options);
101104
storageRpc = options.storageRpc();
102-
// todo: configure timeouts - https://developers.google.com/api-client-library/java/google-api-java-client/errors
103105
// todo: provide rewrite - https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite
104106
// todo: check if we need to expose https://cloud.google.com/storage/docs/json_api/v1/bucketAccessControls/insert vs using bucket update/patch
105107
}
@@ -123,20 +125,33 @@ public com.google.api.services.storage.model.Bucket call() {
123125

124126
@Override
125127
public BlobInfo create(BlobInfo blobInfo, BlobTargetOption... options) {
126-
return create(blobInfo, new ByteArrayInputStream(EMPTY_BYTE_ARRAY), options);
128+
BlobInfo updatedInfo = blobInfo.toBuilder()
129+
.md5(EMPTY_BYTE_ARRAY_MD5)
130+
.crc32c(EMPTY_BYTE_ARRAY_CRC32C)
131+
.build();
132+
return create(updatedInfo, new ByteArrayInputStream(EMPTY_BYTE_ARRAY), options);
127133
}
128134

129135
@Override
130-
public BlobInfo create(BlobInfo blobInfo, final byte[] content, BlobTargetOption... options) {
131-
return create(blobInfo,
132-
new ByteArrayInputStream(firstNonNull(content, EMPTY_BYTE_ARRAY)), options);
136+
public BlobInfo create(BlobInfo blobInfo, byte[] content, BlobTargetOption... options) {
137+
content = firstNonNull(content, EMPTY_BYTE_ARRAY);
138+
BlobInfo updatedInfo = blobInfo.toBuilder()
139+
.md5(BaseEncoding.base64().encode(Hashing.md5().hashBytes(content).asBytes()))
140+
.crc32c(BaseEncoding.base64().encode(
141+
Ints.toByteArray(Hashing.crc32c().hashBytes(content).asInt())))
142+
.build();
143+
return create(updatedInfo, new ByteArrayInputStream(content), options);
133144
}
134145

135146
@Override
136-
public BlobInfo create(BlobInfo blobInfo, final InputStream content,
137-
BlobTargetOption... options) {
138-
final StorageObject blobPb = blobInfo.toPb();
139-
final Map<StorageRpc.Option, ?> optionsMap = optionMap(blobInfo, options);
147+
public BlobInfo create(BlobInfo blobInfo, InputStream content, BlobWriteOption... options) {
148+
Tuple<BlobInfo, BlobTargetOption[]> targetOptions = BlobTargetOption.convert(blobInfo, options);
149+
return create(targetOptions.x(), content, targetOptions.y());
150+
}
151+
152+
private BlobInfo create(BlobInfo info, final InputStream content, BlobTargetOption... options) {
153+
final StorageObject blobPb = info.toPb();
154+
final Map<StorageRpc.Option, ?> optionsMap = optionMap(info, options);
140155
try {
141156
return BlobInfo.fromPb(runWithRetries(new Callable<StorageObject>() {
142157
@Override
@@ -544,7 +559,12 @@ public BlobReadChannel reader(BlobId blob, BlobSourceOption... options) {
544559
}
545560

546561
@Override
547-
public BlobWriteChannel writer(BlobInfo blobInfo, BlobTargetOption... options) {
562+
public BlobWriteChannel writer(BlobInfo blobInfo, BlobWriteOption... options) {
563+
Tuple<BlobInfo, BlobTargetOption[]> targetOptions = BlobTargetOption.convert(blobInfo, options);
564+
return writer(targetOptions.x(), targetOptions.y());
565+
}
566+
567+
private BlobWriteChannel writer(BlobInfo blobInfo, BlobTargetOption... options) {
548568
final Map<StorageRpc.Option, ?> optionsMap = optionMap(blobInfo, options);
549569
return new BlobWriteChannelImpl(options(), blobInfo, optionsMap);
550570
}

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,22 @@ public void testCreateBlobFail() {
145145
assertTrue(storage.delete(bucket, blobName));
146146
}
147147

148+
@Test
149+
public void testCreateBlobMd5Fail() throws UnsupportedEncodingException {
150+
String blobName = "test-create-blob-md5-fail";
151+
BlobInfo blob = BlobInfo.builder(bucket, blobName)
152+
.contentType(CONTENT_TYPE)
153+
.md5("O1R4G1HJSDUISJjoIYmVhQ==")
154+
.build();
155+
ByteArrayInputStream stream = new ByteArrayInputStream(BLOB_STRING_CONTENT.getBytes(UTF_8));
156+
try {
157+
storage.create(blob, stream, Storage.BlobWriteOption.md5Match());
158+
fail("StorageException was expected");
159+
} catch (StorageException ex) {
160+
// expected
161+
}
162+
}
163+
148164
@Test
149165
public void testUpdateBlob() {
150166
String blobName = "test-update-blob";
@@ -449,7 +465,7 @@ public void testWriteChannelFail() throws UnsupportedEncodingException, IOExcept
449465
BlobInfo blob = BlobInfo.builder(bucket, blobName).generation(-1L).build();
450466
try {
451467
try (BlobWriteChannel writer =
452-
storage.writer(blob, Storage.BlobTargetOption.generationMatch())) {
468+
storage.writer(blob, Storage.BlobWriteOption.generationMatch())) {
453469
writer.write(ByteBuffer.allocate(42));
454470
}
455471
fail("StorageException was expected");
@@ -458,6 +474,20 @@ public void testWriteChannelFail() throws UnsupportedEncodingException, IOExcept
458474
}
459475
}
460476

477+
@Test
478+
public void testWriteChannelExistingBlob() throws UnsupportedEncodingException, IOException {
479+
String blobName = "test-write-channel-existing-blob";
480+
BlobInfo blob = BlobInfo.builder(bucket, blobName).build();
481+
BlobInfo remoteBlob = storage.create(blob);
482+
byte[] stringBytes;
483+
try (BlobWriteChannel writer = storage.writer(remoteBlob)) {
484+
stringBytes = BLOB_STRING_CONTENT.getBytes(UTF_8);
485+
writer.write(ByteBuffer.wrap(stringBytes));
486+
}
487+
assertArrayEquals(stringBytes, storage.readAllBytes(blob.blobId()));
488+
assertTrue(storage.delete(bucket, blobName));
489+
}
490+
461491
@Test
462492
public void testGetSignedUrl() throws IOException {
463493
String blobName = "test-get-signed-url-blob";

0 commit comments

Comments
 (0)