diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Bucket.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Bucket.java index 5df305ff371c..e44bd60d785c 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Bucket.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Bucket.java @@ -22,7 +22,6 @@ import static com.google.gcloud.storage.Bucket.BucketSourceOption.toSourceOptions; import com.google.common.base.Function; -import com.google.common.base.MoreObjects; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gcloud.Page; @@ -633,15 +632,13 @@ public List get(String blobName1, String blobName2, String... blobNames) { * * @param blob a blob name * @param content the blob content - * @param contentType the blob content type. If {@code null} then - * {@value com.google.gcloud.storage.Storage#DEFAULT_CONTENT_TYPE} is used. + * @param contentType the blob content type * @param options options for blob creation * @return a complete blob information * @throws StorageException upon failure */ public Blob create(String blob, byte[] content, String contentType, BlobTargetOption... options) { - BlobInfo blobInfo = BlobInfo.builder(BlobId.of(name(), blob)) - .contentType(MoreObjects.firstNonNull(contentType, Storage.DEFAULT_CONTENT_TYPE)).build(); + BlobInfo blobInfo = BlobInfo.builder(BlobId.of(name(), blob)).contentType(contentType).build(); StorageRpc.Tuple target = BlobTargetOption.toTargetOptions(blobInfo, options); return storage.create(target.x(), content, target.y()); @@ -654,16 +651,51 @@ public Blob create(String blob, byte[] content, String contentType, BlobTargetOp * * @param blob a blob name * @param content the blob content as a stream - * @param contentType the blob content type. If {@code null} then - * {@value com.google.gcloud.storage.Storage#DEFAULT_CONTENT_TYPE} is used. + * @param contentType the blob content type * @param options options for blob creation * @return a complete blob information * @throws StorageException upon failure */ public Blob create(String blob, InputStream content, String contentType, BlobWriteOption... options) { - BlobInfo blobInfo = BlobInfo.builder(BlobId.of(name(), blob)) - .contentType(MoreObjects.firstNonNull(contentType, Storage.DEFAULT_CONTENT_TYPE)).build(); + BlobInfo blobInfo = BlobInfo.builder(BlobId.of(name(), blob)).contentType(contentType).build(); + StorageRpc.Tuple write = + BlobWriteOption.toWriteOptions(blobInfo, options); + return storage.create(write.x(), content, write.y()); + } + + /** + * Creates a new blob in this bucket. Direct upload is used to upload {@code content}. + * For large content, {@link Blob#writer(com.google.gcloud.storage.Storage.BlobWriteOption...)} + * is recommended as it uses resumable upload. MD5 and CRC32C hashes of {@code content} are + * computed and used for validating transferred data. + * + * @param blob a blob name + * @param content the blob content + * @param options options for blob creation + * @return a complete blob information + * @throws StorageException upon failure + */ + public Blob create(String blob, byte[] content, BlobTargetOption... options) { + BlobInfo blobInfo = BlobInfo.builder(BlobId.of(name(), blob)).build(); + StorageRpc.Tuple target = + BlobTargetOption.toTargetOptions(blobInfo, options); + return storage.create(target.x(), content, target.y()); + } + + /** + * Creates a new blob in this bucket. Direct upload is used to upload {@code content}. + * For large content, {@link Blob#writer(com.google.gcloud.storage.Storage.BlobWriteOption...)} + * is recommended as it uses resumable upload. + * + * @param blob a blob name + * @param content the blob content as a stream + * @param options options for blob creation + * @return a complete blob information + * @throws StorageException upon failure + */ + public Blob create(String blob, InputStream content, BlobWriteOption... options) { + BlobInfo blobInfo = BlobInfo.builder(BlobId.of(name(), blob)).build(); StorageRpc.Tuple write = BlobWriteOption.toWriteOptions(blobInfo, options); return storage.create(write.x(), content, write.y()); diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/CopyWriter.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/CopyWriter.java index 62b39e005369..743630b6c4c2 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/CopyWriter.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/CopyWriter.java @@ -32,7 +32,13 @@ import java.util.concurrent.Callable; /** - * Google Storage blob copy writer. This class holds the result of a copy request. If source and + * Google Storage blob copy writer. A {@code CopyWriter} object allows to copy both blob's data and + * information. To override source blob's information supply a {@code BlobInfo} to the + * {@code CopyRequest} using either + * {@link Storage.CopyRequest.Builder#target(BlobInfo, Storage.BlobTargetOption...)} or + * {@link Storage.CopyRequest.Builder#target(BlobInfo, Iterable)}. + * + *

This class holds the result of a copy request. If source and * destination blobs share the same location and storage class the copy is completed in one RPC call * otherwise one or more {@link #copyChunk} calls are necessary to complete the copy. In addition, * {@link CopyWriter#result()} can be used to automatically complete the copy and return information @@ -65,11 +71,11 @@ public class CopyWriter implements Restorable { * * @throws StorageException upon failure */ - public BlobInfo result() { + public Blob result() { while (!isDone()) { copyChunk(); } - return BlobInfo.fromPb(rewriteResponse.result); + return Blob.fromPb(serviceOptions.service(), rewriteResponse.result); } /** @@ -120,8 +126,10 @@ public RestorableState capture() { serviceOptions, BlobId.fromPb(rewriteResponse.rewriteRequest.source), rewriteResponse.rewriteRequest.sourceOptions, + rewriteResponse.rewriteRequest.overrideInfo, BlobInfo.fromPb(rewriteResponse.rewriteRequest.target), rewriteResponse.rewriteRequest.targetOptions) + .result(rewriteResponse.result != null ? BlobInfo.fromPb(rewriteResponse.result) : null) .blobSize(blobSize()) .isDone(isDone()) .megabytesCopiedPerChunk(rewriteResponse.rewriteRequest.megabytesRewrittenPerCall) @@ -132,11 +140,12 @@ public RestorableState capture() { static class StateImpl implements RestorableState, Serializable { - private static final long serialVersionUID = 8279287678903181701L; + private static final long serialVersionUID = 1693964441435822700L; private final StorageOptions serviceOptions; private final BlobId source; private final Map sourceOptions; + private final boolean overrideInfo; private final BlobInfo target; private final Map targetOptions; private final BlobInfo result; @@ -150,6 +159,7 @@ static class StateImpl implements RestorableState, Serializable { this.serviceOptions = builder.serviceOptions; this.source = builder.source; this.sourceOptions = builder.sourceOptions; + this.overrideInfo = builder.overrideInfo; this.target = builder.target; this.targetOptions = builder.targetOptions; this.result = builder.result; @@ -165,6 +175,7 @@ static class Builder { private final StorageOptions serviceOptions; private final BlobId source; private final Map sourceOptions; + private final boolean overrideInfo; private final BlobInfo target; private final Map targetOptions; private BlobInfo result; @@ -175,11 +186,12 @@ static class Builder { private Long megabytesCopiedPerChunk; private Builder(StorageOptions options, BlobId source, - Map sourceOptions, - BlobInfo target, Map targetOptions) { + Map sourceOptions, boolean overrideInfo, BlobInfo target, + Map targetOptions) { this.serviceOptions = options; this.source = source; this.sourceOptions = sourceOptions; + this.overrideInfo = overrideInfo; this.target = target; this.targetOptions = targetOptions; } @@ -220,15 +232,15 @@ RestorableState build() { } static Builder builder(StorageOptions options, BlobId source, - Map sourceOptions, BlobInfo target, + Map sourceOptions, boolean overrideInfo, BlobInfo target, Map targetOptions) { - return new Builder(options, source, sourceOptions, target, targetOptions); + return new Builder(options, source, sourceOptions, overrideInfo, target, targetOptions); } @Override public CopyWriter restore() { - RewriteRequest rewriteRequest = new RewriteRequest( - source.toPb(), sourceOptions, target.toPb(), targetOptions, megabytesCopiedPerChunk); + RewriteRequest rewriteRequest = new RewriteRequest(source.toPb(), sourceOptions, + overrideInfo, target.toPb(), targetOptions, megabytesCopiedPerChunk); RewriteResponse rewriteResponse = new RewriteResponse(rewriteRequest, result != null ? result.toPb() : null, blobSize, isDone, rewriteToken, totalBytesCopied); @@ -237,8 +249,9 @@ public CopyWriter restore() { @Override public int hashCode() { - return Objects.hash(serviceOptions, source, sourceOptions, target, targetOptions, result, - blobSize, isDone, megabytesCopiedPerChunk, rewriteToken, totalBytesCopied); + return Objects.hash(serviceOptions, source, sourceOptions, overrideInfo, target, + targetOptions, result, blobSize, isDone, megabytesCopiedPerChunk, rewriteToken, + totalBytesCopied); } @Override @@ -253,6 +266,7 @@ public boolean equals(Object obj) { return Objects.equals(this.serviceOptions, other.serviceOptions) && Objects.equals(this.source, other.source) && Objects.equals(this.sourceOptions, other.sourceOptions) + && Objects.equals(this.overrideInfo, other.overrideInfo) && Objects.equals(this.target, other.target) && Objects.equals(this.targetOptions, other.targetOptions) && Objects.equals(this.result, other.result) @@ -267,10 +281,14 @@ public boolean equals(Object obj) { public String toString() { return MoreObjects.toStringHelper(this) .add("source", source) + .add("overrideInfo", overrideInfo) .add("target", target) - .add("isDone", isDone) - .add("totalBytesRewritten", totalBytesCopied) + .add("result", result) .add("blobSize", blobSize) + .add("isDone", isDone) + .add("rewriteToken", rewriteToken) + .add("totalBytesCopied", totalBytesCopied) + .add("megabytesCopiedPerChunk", megabytesCopiedPerChunk) .toString(); } } diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java index c30111e50835..b4fbe45244b0 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java @@ -53,8 +53,6 @@ */ public interface Storage extends Service { - String DEFAULT_CONTENT_TYPE = "application/octet-stream"; - enum PredefinedAcl { AUTHENTICATED_READ("authenticatedRead"), ALL_AUTHENTICATED_USERS("allAuthenticatedUsers"), @@ -964,6 +962,7 @@ class CopyRequest implements Serializable { private final BlobId source; private final List sourceOptions; + private final boolean overrideInfo; private final BlobInfo target; private final List targetOptions; private final Long megabytesCopiedPerChunk; @@ -973,6 +972,7 @@ public static class Builder { private final Set sourceOptions = new LinkedHashSet<>(); private final Set targetOptions = new LinkedHashSet<>(); private BlobId source; + private boolean overrideInfo; private BlobInfo target; private Long megabytesCopiedPerChunk; @@ -1021,39 +1021,38 @@ public Builder sourceOptions(Iterable options) { * * @return the builder */ - public Builder target(BlobId target) { - this.target = BlobInfo.builder(target).build(); + public Builder target(BlobId targetId) { + this.overrideInfo = false; + this.target = BlobInfo.builder(targetId).build(); return this; } /** * Sets the copy target and target options. {@code target} parameter is used to override - * source blob information (e.g. {@code contentType}, {@code contentLanguage}). {@code - * target.contentType} is a required field. + * source blob information (e.g. {@code contentType}, {@code contentLanguage}). Target blob + * information is set exactly to {@code target}, no information is inherited from the source + * blob. * * @return the builder - * @throws IllegalArgumentException if {@code target.contentType} is {@code null} */ - public Builder target(BlobInfo target, BlobTargetOption... options) - throws IllegalArgumentException { - checkContentType(target); - this.target = target; + public Builder target(BlobInfo target, BlobTargetOption... options) { + this.overrideInfo = true; + this.target = checkNotNull(target); Collections.addAll(targetOptions, options); return this; } /** * Sets the copy target and target options. {@code target} parameter is used to override - * source blob information (e.g. {@code contentType}, {@code contentLanguage}). {@code - * target.contentType} is a required field. + * source blob information (e.g. {@code contentType}, {@code contentLanguage}). Target blob + * information is set exactly to {@code target}, no information is inherited from the source + * blob. * * @return the builder - * @throws IllegalArgumentException if {@code target.contentType} is {@code null} */ - public Builder target(BlobInfo target, Iterable options) - throws IllegalArgumentException { - checkContentType(target); - this.target = target; + public Builder target(BlobInfo target, Iterable options) { + this.overrideInfo = true; + this.target = checkNotNull(target); Iterables.addAll(targetOptions, options); return this; } @@ -1074,8 +1073,6 @@ public Builder megabytesCopiedPerChunk(Long megabytesCopiedPerChunk) { * Creates a {@code CopyRequest} object. */ public CopyRequest build() { - checkNotNull(source); - checkNotNull(target); return new CopyRequest(this); } } @@ -1083,6 +1080,7 @@ public CopyRequest build() { private CopyRequest(Builder builder) { source = checkNotNull(builder.source); sourceOptions = ImmutableList.copyOf(builder.sourceOptions); + overrideInfo = builder.overrideInfo; target = checkNotNull(builder.target); targetOptions = ImmutableList.copyOf(builder.targetOptions); megabytesCopiedPerChunk = builder.megabytesCopiedPerChunk; @@ -1109,6 +1107,17 @@ public BlobInfo target() { return target; } + /** + * Returns whether to override the target blob information with {@link #target()}. + * If {@code true}, the value of {@link #target()} is used to replace source blob information + * (e.g. {@code contentType}, {@code contentLanguage}). Target blob information is set exactly + * to this value, no information is inherited from the source blob. If {@code false}, target + * blob information is inherited from the source blob. + */ + public boolean overrideInfo() { + return overrideInfo; + } + /** * Returns blob's target options. */ @@ -1127,34 +1136,27 @@ public Long megabytesCopiedPerChunk() { /** * Creates a copy request. {@code target} parameter is used to override source blob information - * (e.g. {@code contentType}, {@code contentLanguage}). {@code target.contentType} is a required - * field. + * (e.g. {@code contentType}, {@code contentLanguage}). * * @param sourceBucket name of the bucket containing the source blob * @param sourceBlob name of the source blob * @param target a {@code BlobInfo} object for the target blob * @return a copy request - * @throws IllegalArgumentException if {@code target.contentType} is {@code null} */ - public static CopyRequest of(String sourceBucket, String sourceBlob, BlobInfo target) - throws IllegalArgumentException { - checkContentType(target); + public static CopyRequest of(String sourceBucket, String sourceBlob, BlobInfo target) { return builder().source(sourceBucket, sourceBlob).target(target).build(); } /** - * Creates a copy request. {@code target} parameter is used to override source blob information - * (e.g. {@code contentType}, {@code contentLanguage}). {@code target.contentType} is a required - * field. + * Creates a copy request. {@code target} parameter is used to replace source blob information + * (e.g. {@code contentType}, {@code contentLanguage}). Target blob information is set exactly + * to {@code target}, no information is inherited from the source blob. * * @param sourceBlobId a {@code BlobId} object for the source blob * @param target a {@code BlobInfo} object for the target blob * @return a copy request - * @throws IllegalArgumentException if {@code target.contentType} is {@code null} */ - public static CopyRequest of(BlobId sourceBlobId, BlobInfo target) - throws IllegalArgumentException { - checkContentType(target); + public static CopyRequest of(BlobId sourceBlobId, BlobInfo target) { return builder().source(sourceBlobId).target(target).build(); } @@ -1216,10 +1218,6 @@ public static CopyRequest of(BlobId sourceBlobId, BlobId targetBlobId) { public static Builder builder() { return new Builder(); } - - private static void checkContentType(BlobInfo blobInfo) throws IllegalArgumentException { - checkArgument(blobInfo.contentType() != null, "Blob content type can not be null"); - } } /** @@ -1387,12 +1385,18 @@ private static void checkContentType(BlobInfo blobInfo) throws IllegalArgumentEx Blob compose(ComposeRequest composeRequest); /** - * Sends a copy request. Returns a {@link CopyWriter} object for the provided - * {@code CopyRequest}. If source and destination objects share the same location and storage - * class the source blob is copied with one request and {@link CopyWriter#result()} immediately - * returns, regardless of the {@link CopyRequest#megabytesCopiedPerChunk} parameter. - * If source and destination have different location or storage class {@link CopyWriter#result()} - * might issue multiple RPC calls depending on blob's size. + * Sends a copy request. This method copies both blob's data and information. To override source + * blob's information supply a {@code BlobInfo} to the + * {@code CopyRequest} using either + * {@link Storage.CopyRequest.Builder#target(BlobInfo, Storage.BlobTargetOption...)} or + * {@link Storage.CopyRequest.Builder#target(BlobInfo, Iterable)}. + * + *

This method returns a {@link CopyWriter} object for the provided {@code CopyRequest}. If + * source and destination objects share the same location and storage class the source blob is + * copied with one request and {@link CopyWriter#result()} immediately returns, regardless of the + * {@link CopyRequest#megabytesCopiedPerChunk} parameter. If source and destination have different + * location or storage class {@link CopyWriter#result()} might issue multiple RPC calls depending + * on blob's size. * *

Example usage of copy: *

 {@code BlobInfo blob = service.copy(copyRequest).result();}
diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java
index d58c9e43aea9..cf709ba5e293 100644
--- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java
+++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java
@@ -413,15 +413,16 @@ public CopyWriter copy(final CopyRequest copyRequest) {
     final StorageObject source = copyRequest.source().toPb();
     final Map sourceOptions =
         optionMap(copyRequest.source().generation(), null, copyRequest.sourceOptions(), true);
-    final StorageObject target = copyRequest.target().toPb();
+    final StorageObject targetObject = copyRequest.target().toPb();
     final Map targetOptions = optionMap(copyRequest.target().generation(),
         copyRequest.target().metageneration(), copyRequest.targetOptions());
     try {
       RewriteResponse rewriteResponse = runWithRetries(new Callable() {
         @Override
         public RewriteResponse call() {
-          return storageRpc.openRewrite(new StorageRpc.RewriteRequest(source, sourceOptions, target,
-              targetOptions, copyRequest.megabytesCopiedPerChunk()));
+          return storageRpc.openRewrite(new StorageRpc.RewriteRequest(source, sourceOptions,
+              copyRequest.overrideInfo(), targetObject, targetOptions,
+              copyRequest.megabytesCopiedPerChunk()));
         }
       }, options().retryParams(), EXCEPTION_HANDLER);
       return new CopyWriter(options(), rewriteResponse);
diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/spi/DefaultStorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/spi/DefaultStorageRpc.java
index aa6085e161ed..8d06832534e2 100644
--- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/spi/DefaultStorageRpc.java
+++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/spi/DefaultStorageRpc.java
@@ -319,9 +319,6 @@ private Storage.Objects.Delete deleteRequest(StorageObject blob, Map
   public StorageObject compose(Iterable sources, StorageObject target,
       Map targetOptions) {
     ComposeRequest request = new ComposeRequest();
-    if (target.getContentType() == null) {
-      target.setContentType("application/octet-stream");
-    }
     request.setDestination(target);
     List sourceObjects = new ArrayList<>();
     for (StorageObject source : sources) {
@@ -584,7 +581,7 @@ private RewriteResponse rewrite(RewriteRequest req, String token) {
           ? req.megabytesRewrittenPerCall * MEGABYTE : null;
       com.google.api.services.storage.model.RewriteResponse rewriteResponse = storage.objects()
           .rewrite(req.source.getBucket(), req.source.getName(), req.target.getBucket(),
-              req.target.getName(), req.target.getContentType() != null ? req.target : null)
+              req.target.getName(), req.overrideInfo ? req.target : null)
           .setSourceGeneration(req.source.getGeneration())
           .setRewriteToken(token)
           .setMaxBytesRewrittenPerCall(maxBytesRewrittenPerCall)
diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/spi/StorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/spi/StorageRpc.java
index d239a475a6dd..74f8171de87f 100644
--- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/spi/StorageRpc.java
+++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/spi/StorageRpc.java
@@ -138,15 +138,17 @@ class RewriteRequest {
 
     public final StorageObject source;
     public final Map sourceOptions;
+    public final boolean overrideInfo;
     public final StorageObject target;
     public final Map targetOptions;
     public final Long megabytesRewrittenPerCall;
 
     public RewriteRequest(StorageObject source, Map sourceOptions,
-        StorageObject target, Map targetOptions,
+        boolean overrideInfo, StorageObject target, Map targetOptions,
         Long megabytesRewrittenPerCall) {
       this.source = source;
       this.sourceOptions = sourceOptions;
+      this.overrideInfo = overrideInfo;
       this.target = target;
       this.targetOptions = targetOptions;
       this.megabytesRewrittenPerCall = megabytesRewrittenPerCall;
@@ -163,6 +165,7 @@ public boolean equals(Object obj) {
       final RewriteRequest other = (RewriteRequest) obj;
       return Objects.equals(this.source, other.source)
           && Objects.equals(this.sourceOptions, other.sourceOptions)
+          && Objects.equals(this.overrideInfo, other.overrideInfo)
           && Objects.equals(this.target, other.target)
           && Objects.equals(this.targetOptions, other.targetOptions)
           && Objects.equals(this.megabytesRewrittenPerCall, other.megabytesRewrittenPerCall);
@@ -170,7 +173,8 @@ public boolean equals(Object obj) {
 
     @Override
     public int hashCode() {
-      return Objects.hash(source, sourceOptions, target, targetOptions, megabytesRewrittenPerCall);
+      return Objects.hash(source, sourceOptions, overrideInfo, target, targetOptions,
+          megabytesRewrittenPerCall);
     }
   }
 
diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobTest.java
index 5a6173c08199..d6c97ca9ca03 100644
--- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobTest.java
+++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BlobTest.java
@@ -233,6 +233,7 @@ public void testCopyToBucket() throws Exception {
     assertEquals(copyWriter, returnedCopyWriter);
     assertEquals(capturedCopyRequest.getValue().source(), blob.blobId());
     assertEquals(capturedCopyRequest.getValue().target(), target);
+    assertFalse(capturedCopyRequest.getValue().overrideInfo());
     assertTrue(capturedCopyRequest.getValue().sourceOptions().isEmpty());
     assertTrue(capturedCopyRequest.getValue().targetOptions().isEmpty());
   }
@@ -251,6 +252,7 @@ public void testCopyTo() throws Exception {
     assertEquals(copyWriter, returnedCopyWriter);
     assertEquals(capturedCopyRequest.getValue().source(), blob.blobId());
     assertEquals(capturedCopyRequest.getValue().target(), target);
+    assertFalse(capturedCopyRequest.getValue().overrideInfo());
     assertTrue(capturedCopyRequest.getValue().sourceOptions().isEmpty());
     assertTrue(capturedCopyRequest.getValue().targetOptions().isEmpty());
   }
@@ -258,9 +260,9 @@ public void testCopyTo() throws Exception {
   @Test
   public void testCopyToBlobId() throws Exception {
     initializeExpectedBlob(2);
+    BlobInfo target = BlobInfo.builder(BlobId.of("bt", "nt")).build();
     BlobId targetId = BlobId.of("bt", "nt");
     CopyWriter copyWriter = createMock(CopyWriter.class);
-    BlobInfo target = BlobInfo.builder(targetId).build();
     Capture capturedCopyRequest = Capture.newInstance();
     expect(storage.options()).andReturn(mockOptions);
     expect(storage.copy(capture(capturedCopyRequest))).andReturn(copyWriter);
@@ -270,6 +272,7 @@ public void testCopyToBlobId() throws Exception {
     assertEquals(copyWriter, returnedCopyWriter);
     assertEquals(capturedCopyRequest.getValue().source(), blob.blobId());
     assertEquals(capturedCopyRequest.getValue().target(), target);
+    assertFalse(capturedCopyRequest.getValue().overrideInfo());
     assertTrue(capturedCopyRequest.getValue().sourceOptions().isEmpty());
     assertTrue(capturedCopyRequest.getValue().targetOptions().isEmpty());
   }
diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketTest.java
index 236411e0c2d8..53056c39c0dc 100644
--- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketTest.java
+++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/BucketTest.java
@@ -293,16 +293,16 @@ public void testCreate() throws Exception {
   }
 
   @Test
-  public void testCreateNullContentType() throws Exception {
+  public void testCreateNoContentType() throws Exception {
     initializeExpectedBucket(5);
-    BlobInfo info = BlobInfo.builder("b", "n").contentType(Storage.DEFAULT_CONTENT_TYPE).build();
+    BlobInfo info = BlobInfo.builder("b", "n").build();
     Blob expectedBlob = new Blob(serviceMockReturnsOptions, new BlobInfo.BuilderImpl(info));
     byte[] content = {0xD, 0xE, 0xA, 0xD};
     expect(storage.options()).andReturn(mockOptions);
     expect(storage.create(info, content)).andReturn(expectedBlob);
     replay(storage);
     initializeBucket();
-    Blob blob = bucket.create("n", content, null);
+    Blob blob = bucket.create("n", content);
     assertEquals(expectedBlob, blob);
   }
 
@@ -388,9 +388,9 @@ public void testCreateFromStream() throws Exception {
   }
 
   @Test
-  public void testCreateFromStreamNullContentType() throws Exception {
+  public void testCreateFromStreamNoContentType() throws Exception {
     initializeExpectedBucket(5);
-    BlobInfo info = BlobInfo.builder("b", "n").contentType(Storage.DEFAULT_CONTENT_TYPE).build();
+    BlobInfo info = BlobInfo.builder("b", "n").build();
     Blob expectedBlob = new Blob(serviceMockReturnsOptions, new BlobInfo.BuilderImpl(info));
     byte[] content = {0xD, 0xE, 0xA, 0xD};
     InputStream streamContent = new ByteArrayInputStream(content);
@@ -398,7 +398,7 @@ public void testCreateFromStreamNullContentType() throws Exception {
     expect(storage.create(info, streamContent)).andReturn(expectedBlob);
     replay(storage);
     initializeBucket();
-    Blob blob = bucket.create("n", streamContent, null);
+    Blob blob = bucket.create("n", streamContent);
     assertEquals(expectedBlob, blob);
   }
 
diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CopyRequestTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CopyRequestTest.java
index b7e8d14e53a1..9f8edfb84162 100644
--- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CopyRequestTest.java
+++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CopyRequestTest.java
@@ -18,6 +18,8 @@
 
 import static com.google.gcloud.storage.Storage.PredefinedAcl.PUBLIC_READ;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gcloud.storage.Storage.BlobSourceOption;
@@ -53,6 +55,7 @@ public void testCopyRequest() {
     assertEquals(1, copyRequest1.sourceOptions().size());
     assertEquals(BlobSourceOption.generationMatch(1), copyRequest1.sourceOptions().get(0));
     assertEquals(TARGET_BLOB_INFO, copyRequest1.target());
+    assertTrue(copyRequest1.overrideInfo());
     assertEquals(1, copyRequest1.targetOptions().size());
     assertEquals(BlobTargetOption.predefinedAcl(PUBLIC_READ), copyRequest1.targetOptions().get(0));
 
@@ -62,6 +65,7 @@ public void testCopyRequest() {
         .build();
     assertEquals(SOURCE_BLOB_ID, copyRequest2.source());
     assertEquals(BlobInfo.builder(TARGET_BLOB_ID).build(), copyRequest2.target());
+    assertFalse(copyRequest2.overrideInfo());
 
     Storage.CopyRequest copyRequest3 = Storage.CopyRequest.builder()
         .source(SOURCE_BLOB_ID)
@@ -69,6 +73,7 @@ public void testCopyRequest() {
         .build();
     assertEquals(SOURCE_BLOB_ID, copyRequest3.source());
     assertEquals(TARGET_BLOB_INFO, copyRequest3.target());
+    assertTrue(copyRequest3.overrideInfo());
     assertEquals(ImmutableList.of(BlobTargetOption.predefinedAcl(PUBLIC_READ)),
         copyRequest3.targetOptions());
   }
@@ -78,52 +83,36 @@ public void testCopyRequestOf() {
     Storage.CopyRequest copyRequest1 = Storage.CopyRequest.of(SOURCE_BLOB_ID, TARGET_BLOB_INFO);
     assertEquals(SOURCE_BLOB_ID, copyRequest1.source());
     assertEquals(TARGET_BLOB_INFO, copyRequest1.target());
+    assertTrue(copyRequest1.overrideInfo());
 
     Storage.CopyRequest copyRequest2 = Storage.CopyRequest.of(SOURCE_BLOB_ID, TARGET_BLOB_NAME);
     assertEquals(SOURCE_BLOB_ID, copyRequest2.source());
-    assertEquals(BlobInfo.builder(SOURCE_BUCKET_NAME, TARGET_BLOB_NAME).build(),
+    assertEquals(BlobInfo.builder(BlobId.of(SOURCE_BUCKET_NAME, TARGET_BLOB_NAME)).build(),
         copyRequest2.target());
+    assertFalse(copyRequest2.overrideInfo());
 
     Storage.CopyRequest copyRequest3 =
         Storage.CopyRequest.of(SOURCE_BUCKET_NAME, SOURCE_BLOB_NAME, TARGET_BLOB_INFO);
     assertEquals(SOURCE_BLOB_ID, copyRequest3.source());
     assertEquals(TARGET_BLOB_INFO, copyRequest3.target());
+    assertTrue(copyRequest3.overrideInfo());
 
     Storage.CopyRequest copyRequest4 =
         Storage.CopyRequest.of(SOURCE_BUCKET_NAME, SOURCE_BLOB_NAME, TARGET_BLOB_NAME);
     assertEquals(SOURCE_BLOB_ID, copyRequest4.source());
-    assertEquals(BlobInfo.builder(SOURCE_BUCKET_NAME, TARGET_BLOB_NAME).build(),
+    assertEquals(BlobInfo.builder(BlobId.of(SOURCE_BUCKET_NAME, TARGET_BLOB_NAME)).build(),
         copyRequest4.target());
+    assertFalse(copyRequest4.overrideInfo());
 
     Storage.CopyRequest copyRequest5 = Storage.CopyRequest.of(SOURCE_BLOB_ID, TARGET_BLOB_ID);
     assertEquals(SOURCE_BLOB_ID, copyRequest5.source());
     assertEquals(BlobInfo.builder(TARGET_BLOB_ID).build(), copyRequest5.target());
+    assertFalse(copyRequest5.overrideInfo());
 
     Storage.CopyRequest copyRequest6 =
         Storage.CopyRequest.of(SOURCE_BUCKET_NAME, SOURCE_BLOB_NAME, TARGET_BLOB_ID);
     assertEquals(SOURCE_BLOB_ID, copyRequest6.source());
     assertEquals(BlobInfo.builder(TARGET_BLOB_ID).build(), copyRequest6.target());
-  }
-
-  @Test
-  public void testCopyRequestFail() {
-    thrown.expect(IllegalArgumentException.class);
-    Storage.CopyRequest.builder()
-        .source(SOURCE_BLOB_ID)
-        .target(BlobInfo.builder(TARGET_BLOB_ID).build())
-        .build();
-  }
-
-  @Test
-  public void testCopyRequestOfBlobInfoFail() {
-    thrown.expect(IllegalArgumentException.class);
-    Storage.CopyRequest.of(SOURCE_BLOB_ID, BlobInfo.builder(TARGET_BLOB_ID).build());
-  }
-
-  @Test
-  public void testCopyRequestOfStringFail() {
-    thrown.expect(IllegalArgumentException.class);
-    Storage.CopyRequest.of(
-        SOURCE_BUCKET_NAME, SOURCE_BLOB_NAME, BlobInfo.builder(TARGET_BLOB_ID).build());
+    assertFalse(copyRequest6.overrideInfo());
   }
 }
diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CopyWriterTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CopyWriterTest.java
index ad4a04c34127..8ccb81688b65 100644
--- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CopyWriterTest.java
+++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CopyWriterTest.java
@@ -48,20 +48,29 @@ public class CopyWriterTest {
   private static final BlobId BLOB_ID = BlobId.of(SOURCE_BUCKET_NAME, SOURCE_BLOB_NAME);
   private static final BlobInfo BLOB_INFO =
       BlobInfo.builder(DESTINATION_BUCKET_NAME, DESTINATION_BLOB_NAME).build();
-  private static final BlobInfo RESULT =
+  private static final BlobInfo RESULT_INFO =
       BlobInfo.builder(DESTINATION_BUCKET_NAME, DESTINATION_BLOB_NAME).contentType("type").build();
   private static final Map EMPTY_OPTIONS = ImmutableMap.of();
-  private static final RewriteRequest REQUEST = new StorageRpc.RewriteRequest(BLOB_ID.toPb(),
-      EMPTY_OPTIONS, BLOB_INFO.toPb(), EMPTY_OPTIONS, null);
-  private static final RewriteResponse RESPONSE = new StorageRpc.RewriteResponse(REQUEST,
-      null, 42L, false, "token", 21L);
-  private static final RewriteResponse RESPONSE_DONE = new StorageRpc.RewriteResponse(REQUEST,
-      RESULT.toPb(), 42L, true, "token", 42L);
+  private static final RewriteRequest REQUEST_WITH_OBJECT =
+      new StorageRpc.RewriteRequest(BLOB_ID.toPb(), EMPTY_OPTIONS, true, BLOB_INFO.toPb(),
+          EMPTY_OPTIONS, null);
+  private static final RewriteRequest REQUEST_WITHOUT_OBJECT =
+      new StorageRpc.RewriteRequest(BLOB_ID.toPb(), EMPTY_OPTIONS, false, BLOB_INFO.toPb(),
+          EMPTY_OPTIONS, null);
+  private static final RewriteResponse RESPONSE_WITH_OBJECT = new RewriteResponse(
+      REQUEST_WITH_OBJECT, null, 42L, false, "token", 21L);
+  private static final RewriteResponse RESPONSE_WITHOUT_OBJECT = new RewriteResponse(
+      REQUEST_WITHOUT_OBJECT, null, 42L, false, "token", 21L);
+  private static final RewriteResponse RESPONSE_WITH_OBJECT_DONE =
+      new RewriteResponse(REQUEST_WITH_OBJECT, RESULT_INFO.toPb(), 42L, true, "token", 42L);
+  private static final RewriteResponse RESPONSE_WITHOUT_OBJECT_DONE =
+      new RewriteResponse(REQUEST_WITHOUT_OBJECT, RESULT_INFO.toPb(), 42L, true, "token", 42L);
 
   private StorageOptions options;
   private StorageRpcFactory rpcFactoryMock;
   private StorageRpc storageRpcMock;
   private CopyWriter copyWriter;
+  private Blob result;
 
   @Before
   public void setUp() {
@@ -75,6 +84,7 @@ public void setUp() {
         .serviceRpcFactory(rpcFactoryMock)
         .retryParams(RetryParams.noRetries())
         .build();
+    result = new Blob(options.service(), new BlobInfo.BuilderImpl(RESULT_INFO));
   }
 
   @After
@@ -83,41 +93,111 @@ public void tearDown() throws Exception {
   }
 
   @Test
-  public void testRewrite() {
-    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE)).andReturn(RESPONSE_DONE);
+  public void testRewriteWithObject() {
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITH_OBJECT))
+        .andReturn(RESPONSE_WITH_OBJECT_DONE);
     EasyMock.replay(storageRpcMock);
-    copyWriter = new CopyWriter(options, RESPONSE);
-    assertEquals(RESULT, copyWriter.result());
+    copyWriter = new CopyWriter(options, RESPONSE_WITH_OBJECT);
+    assertEquals(result, copyWriter.result());
     assertTrue(copyWriter.isDone());
     assertEquals(42L, copyWriter.totalBytesCopied());
     assertEquals(42L, copyWriter.blobSize());
   }
 
   @Test
-  public void testRewriteMultipleRequests() {
-    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE)).andReturn(RESPONSE);
-    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE)).andReturn(RESPONSE_DONE);
+  public void testRewriteWithoutObject() {
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITHOUT_OBJECT))
+        .andReturn(RESPONSE_WITHOUT_OBJECT_DONE);
     EasyMock.replay(storageRpcMock);
-    copyWriter = new CopyWriter(options, RESPONSE);
-    assertEquals(RESULT, copyWriter.result());
+    copyWriter = new CopyWriter(options, RESPONSE_WITHOUT_OBJECT);
+    assertEquals(result, copyWriter.result());
     assertTrue(copyWriter.isDone());
     assertEquals(42L, copyWriter.totalBytesCopied());
     assertEquals(42L, copyWriter.blobSize());
   }
 
   @Test
-  public void testSaveAndRestore() {
-    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE)).andReturn(RESPONSE);
-    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE)).andReturn(RESPONSE_DONE);
+  public void testRewriteWithObjectMultipleRequests() {
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITH_OBJECT))
+        .andReturn(RESPONSE_WITH_OBJECT);
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITH_OBJECT))
+        .andReturn(RESPONSE_WITH_OBJECT_DONE);
     EasyMock.replay(storageRpcMock);
-    copyWriter = new CopyWriter(options, RESPONSE);
+    copyWriter = new CopyWriter(options, RESPONSE_WITH_OBJECT);
+    assertEquals(result, copyWriter.result());
+    assertTrue(copyWriter.isDone());
+    assertEquals(42L, copyWriter.totalBytesCopied());
+    assertEquals(42L, copyWriter.blobSize());
+  }
+
+  @Test
+  public void testRewriteWithoutObjectMultipleRequests() {
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITHOUT_OBJECT))
+        .andReturn(RESPONSE_WITHOUT_OBJECT);
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITHOUT_OBJECT))
+        .andReturn(RESPONSE_WITHOUT_OBJECT_DONE);
+    EasyMock.replay(storageRpcMock);
+    copyWriter = new CopyWriter(options, RESPONSE_WITHOUT_OBJECT);
+    assertEquals(result, copyWriter.result());
+    assertTrue(copyWriter.isDone());
+    assertEquals(42L, copyWriter.totalBytesCopied());
+    assertEquals(42L, copyWriter.blobSize());
+  }
+
+  @Test
+  public void testSaveAndRestoreWithObject() {
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITH_OBJECT))
+        .andReturn(RESPONSE_WITH_OBJECT);
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITH_OBJECT))
+        .andReturn(RESPONSE_WITH_OBJECT_DONE);
+    EasyMock.replay(storageRpcMock);
+    copyWriter = new CopyWriter(options, RESPONSE_WITH_OBJECT);
+    copyWriter.copyChunk();
+    assertTrue(!copyWriter.isDone());
+    assertEquals(21L, copyWriter.totalBytesCopied());
+    assertEquals(42L, copyWriter.blobSize());
+    RestorableState rewriterState = copyWriter.capture();
+    CopyWriter restoredRewriter = rewriterState.restore();
+    assertEquals(result, restoredRewriter.result());
+    assertTrue(restoredRewriter.isDone());
+    assertEquals(42L, restoredRewriter.totalBytesCopied());
+    assertEquals(42L, restoredRewriter.blobSize());
+  }
+
+  @Test
+  public void testSaveAndRestoreWithoutObject() {
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITHOUT_OBJECT))
+        .andReturn(RESPONSE_WITHOUT_OBJECT);
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITHOUT_OBJECT))
+        .andReturn(RESPONSE_WITHOUT_OBJECT_DONE);
+    EasyMock.replay(storageRpcMock);
+    copyWriter = new CopyWriter(options, RESPONSE_WITHOUT_OBJECT);
     copyWriter.copyChunk();
     assertTrue(!copyWriter.isDone());
     assertEquals(21L, copyWriter.totalBytesCopied());
     assertEquals(42L, copyWriter.blobSize());
     RestorableState rewriterState = copyWriter.capture();
     CopyWriter restoredRewriter = rewriterState.restore();
-    assertEquals(RESULT, restoredRewriter.result());
+    assertEquals(result, restoredRewriter.result());
+    assertTrue(restoredRewriter.isDone());
+    assertEquals(42L, restoredRewriter.totalBytesCopied());
+    assertEquals(42L, restoredRewriter.blobSize());
+  }
+
+  @Test
+  public void testSaveAndRestoreWithResult() {
+    EasyMock.expect(storageRpcMock.continueRewrite(RESPONSE_WITH_OBJECT))
+        .andReturn(RESPONSE_WITH_OBJECT_DONE);
+    EasyMock.replay(storageRpcMock);
+    copyWriter = new CopyWriter(options, RESPONSE_WITH_OBJECT);
+    copyWriter.copyChunk();
+    assertEquals(result, copyWriter.result());
+    assertTrue(copyWriter.isDone());
+    assertEquals(42L, copyWriter.totalBytesCopied());
+    assertEquals(42L, copyWriter.blobSize());
+    RestorableState rewriterState = copyWriter.capture();
+    CopyWriter restoredRewriter = rewriterState.restore();
+    assertEquals(result, restoredRewriter.result());
     assertTrue(restoredRewriter.isDone());
     assertEquals(42L, restoredRewriter.totalBytesCopied());
     assertEquals(42L, restoredRewriter.blobSize());
diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/StorageImplTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/StorageImplTest.java
index 38b4bb58e77f..3cc99e3bf884 100644
--- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/StorageImplTest.java
+++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/StorageImplTest.java
@@ -866,7 +866,7 @@ public void testComposeWithOptions() {
   public void testCopy() {
     CopyRequest request = Storage.CopyRequest.of(BLOB_INFO1.blobId(), BLOB_INFO2.blobId());
     StorageRpc.RewriteRequest rpcRequest = new StorageRpc.RewriteRequest(request.source().toPb(),
-        EMPTY_RPC_OPTIONS, request.target().toPb(), EMPTY_RPC_OPTIONS, null);
+        EMPTY_RPC_OPTIONS, false, BLOB_INFO2.toPb(), EMPTY_RPC_OPTIONS, null);
     StorageRpc.RewriteResponse rpcResponse = new StorageRpc.RewriteResponse(rpcRequest, null, 42L,
         false, "token", 21L);
     EasyMock.expect(storageRpcMock.openRewrite(rpcRequest)).andReturn(rpcResponse);
@@ -886,7 +886,7 @@ public void testCopyWithOptions() {
         .target(BLOB_INFO1, BLOB_TARGET_GENERATION, BLOB_TARGET_METAGENERATION)
         .build();
     StorageRpc.RewriteRequest rpcRequest = new StorageRpc.RewriteRequest(request.source().toPb(),
-        BLOB_SOURCE_OPTIONS_COPY, request.target().toPb(), BLOB_TARGET_OPTIONS_COMPOSE, null);
+        BLOB_SOURCE_OPTIONS_COPY, true, request.target().toPb(), BLOB_TARGET_OPTIONS_COMPOSE, null);
     StorageRpc.RewriteResponse rpcResponse = new StorageRpc.RewriteResponse(rpcRequest, null, 42L,
         false, "token", 21L);
     EasyMock.expect(storageRpcMock.openRewrite(rpcRequest)).andReturn(rpcResponse);
@@ -906,7 +906,7 @@ public void testCopyWithOptionsFromBlobId() {
         .target(BLOB_INFO1, BLOB_TARGET_GENERATION, BLOB_TARGET_METAGENERATION)
         .build();
     StorageRpc.RewriteRequest rpcRequest = new StorageRpc.RewriteRequest(request.source().toPb(),
-        BLOB_SOURCE_OPTIONS_COPY, request.target().toPb(), BLOB_TARGET_OPTIONS_COMPOSE, null);
+        BLOB_SOURCE_OPTIONS_COPY, true, request.target().toPb(), BLOB_TARGET_OPTIONS_COMPOSE, null);
     StorageRpc.RewriteResponse rpcResponse =
         new StorageRpc.RewriteResponse(rpcRequest, null, 42L, false, "token", 21L);
     EasyMock.expect(storageRpcMock.openRewrite(rpcRequest)).andReturn(rpcResponse);
@@ -922,7 +922,7 @@ public void testCopyWithOptionsFromBlobId() {
   public void testCopyMultipleRequests() {
     CopyRequest request = Storage.CopyRequest.of(BLOB_INFO1.blobId(), BLOB_INFO2.blobId());
     StorageRpc.RewriteRequest rpcRequest = new StorageRpc.RewriteRequest(request.source().toPb(),
-        EMPTY_RPC_OPTIONS, request.target().toPb(), EMPTY_RPC_OPTIONS, null);
+        EMPTY_RPC_OPTIONS, false, BLOB_INFO2.toPb(), EMPTY_RPC_OPTIONS, null);
     StorageRpc.RewriteResponse rpcResponse1 = new StorageRpc.RewriteResponse(rpcRequest, null, 42L,
         false, "token", 21L);
     StorageRpc.RewriteResponse rpcResponse2 = new StorageRpc.RewriteResponse(rpcRequest,
@@ -935,7 +935,7 @@ public void testCopyMultipleRequests() {
     assertEquals(42L, writer.blobSize());
     assertEquals(21L, writer.totalBytesCopied());
     assertTrue(!writer.isDone());
-    assertEquals(BLOB_INFO1, writer.result());
+    assertEquals(expectedBlob1, writer.result());
     assertTrue(writer.isDone());
     assertEquals(42L, writer.totalBytesCopied());
     assertEquals(42L, writer.blobSize());
diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/it/ITStorageTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/it/ITStorageTest.java
index 563a621c48fb..13d768442c34 100644
--- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/it/ITStorageTest.java
+++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/it/ITStorageTest.java
@@ -599,6 +599,37 @@ public void testComposeBlob() {
     assertNotNull(remoteTargetBlob);
     assertEquals(targetBlob.name(), remoteTargetBlob.name());
     assertEquals(targetBlob.bucket(), remoteTargetBlob.bucket());
+    assertNull(remoteTargetBlob.contentType());
+    byte[] readBytes = storage.readAllBytes(BUCKET, targetBlobName);
+    byte[] composedBytes = Arrays.copyOf(BLOB_BYTE_CONTENT, BLOB_BYTE_CONTENT.length * 2);
+    System.arraycopy(BLOB_BYTE_CONTENT, 0, composedBytes, BLOB_BYTE_CONTENT.length,
+        BLOB_BYTE_CONTENT.length);
+    assertArrayEquals(composedBytes, readBytes);
+    assertTrue(remoteSourceBlob1.delete());
+    assertTrue(remoteSourceBlob2.delete());
+    assertTrue(remoteTargetBlob.delete());
+  }
+
+  @Test
+  public void testComposeBlobWithContentType() {
+    String sourceBlobName1 = "test-compose-blob-with-content-type-source-1";
+    String sourceBlobName2 = "test-compose-blob-with-content-type-source-2";
+    BlobInfo sourceBlob1 = BlobInfo.builder(BUCKET, sourceBlobName1).build();
+    BlobInfo sourceBlob2 = BlobInfo.builder(BUCKET, sourceBlobName2).build();
+    Blob remoteSourceBlob1 = storage.create(sourceBlob1, BLOB_BYTE_CONTENT);
+    Blob remoteSourceBlob2 = storage.create(sourceBlob2, BLOB_BYTE_CONTENT);
+    assertNotNull(remoteSourceBlob1);
+    assertNotNull(remoteSourceBlob2);
+    String targetBlobName = "test-compose-blob-with-content-type-target";
+    BlobInfo targetBlob =
+        BlobInfo.builder(BUCKET, targetBlobName).contentType(CONTENT_TYPE).build();
+    Storage.ComposeRequest req =
+        Storage.ComposeRequest.of(ImmutableList.of(sourceBlobName1, sourceBlobName2), targetBlob);
+    Blob remoteTargetBlob = storage.compose(req);
+    assertNotNull(remoteTargetBlob);
+    assertEquals(targetBlob.name(), remoteTargetBlob.name());
+    assertEquals(targetBlob.bucket(), remoteTargetBlob.bucket());
+    assertEquals(CONTENT_TYPE, remoteTargetBlob.contentType());
     byte[] readBytes = storage.readAllBytes(BUCKET, targetBlobName);
     byte[] composedBytes = Arrays.copyOf(BLOB_BYTE_CONTENT, BLOB_BYTE_CONTENT.length * 2);
     System.arraycopy(BLOB_BYTE_CONTENT, 0, composedBytes, BLOB_BYTE_CONTENT.length,
@@ -682,6 +713,26 @@ public void testCopyBlobUpdateMetadata() {
     assertTrue(storage.delete(BUCKET, targetBlobName));
   }
 
+  @Test
+  public void testCopyBlobNoContentType() {
+    String sourceBlobName = "test-copy-blob-no-content-type-source";
+    BlobId source = BlobId.of(BUCKET, sourceBlobName);
+    Blob remoteSourceBlob = storage.create(BlobInfo.builder(source).build(), BLOB_BYTE_CONTENT);
+    assertNotNull(remoteSourceBlob);
+    String targetBlobName = "test-copy-blob-no-content-type-target";
+    ImmutableMap metadata = ImmutableMap.of("k", "v");
+    BlobInfo target = BlobInfo.builder(BUCKET, targetBlobName).metadata(metadata).build();
+    Storage.CopyRequest req = Storage.CopyRequest.of(source, target);
+    CopyWriter copyWriter = storage.copy(req);
+    assertEquals(BUCKET, copyWriter.result().bucket());
+    assertEquals(targetBlobName, copyWriter.result().name());
+    assertNull(copyWriter.result().contentType());
+    assertEquals(metadata, copyWriter.result().metadata());
+    assertTrue(copyWriter.isDone());
+    assertTrue(remoteSourceBlob.delete());
+    assertTrue(storage.delete(BUCKET, targetBlobName));
+  }
+
   @Test
   public void testCopyBlobFail() {
     String sourceBlobName = "test-copy-blob-source-fail";