Repository: googleapis/java-storage
Pull request #431 (2020-07-14 18:06:58)

chore(deps): update dependency com.google.cloud:libraries-bom to v8.1.0
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [com.google.cloud:libraries-bom](https://togithub.com/GoogleCloudPlatform/cloud-opensource-java) | minor | `8.0.0` -> `8.1.0` |

---

### Renovate configuration

:date: **Schedule**: At any time (no schedule defined).

:vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

:recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

:no_bell: **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#googleapis/java-storage).

codecov[bot]: # [Codecov](https://codecov.io/gh/googleapis/java-storage/pull/431?src=pr&el=h1) Report
> Merging [#431](https://codecov.io/gh/googleapis/java-storage/pull/431?src=pr&el=desc) into [master](https://codecov.io/gh/googleapis/java-storage/commit/12bd62c18ced79e039310e3e78fbd9341ca59384&el=desc) will **decrease** coverage by `0.13%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/googleapis/java-storage/pull/431/graphs/tree.svg?width=650&height=150&src=pr&token=ssm139YOdp)](https://codecov.io/gh/googleapis/java-storage/pull/431?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##             master     #431      +/-   ##
============================================
- Coverage     63.27%   63.13%   -0.14%     
+ Complexity      609      608       -1     
============================================
  Files            32       32              
  Lines          5113     5097      -16     
  Branches        487      455      -32     
============================================
- Hits           3235     3218      -17     
+ Misses         1713     1711       -2     
- Partials        165      168       +3     
```


| [Impacted Files](https://codecov.io/gh/googleapis/java-storage/pull/431?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...in/java/com/google/cloud/storage/StorageBatch.java](https://codecov.io/gh/googleapis/java-storage/pull/431/diff?src=pr&el=tree#diff-Z29vZ2xlLWNsb3VkLXN0b3JhZ2Uvc3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3N0b3JhZ2UvU3RvcmFnZUJhdGNoLmphdmE=) | `88.00% <0.00%> (-4.00%)` | `13.00% <0.00%> (ø%)` | |
| [...gle/cloud/storage/testing/RemoteStorageHelper.java](https://codecov.io/gh/googleapis/java-storage/pull/431/diff?src=pr&el=tree#diff-Z29vZ2xlLWNsb3VkLXN0b3JhZ2Uvc3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3N0b3JhZ2UvdGVzdGluZy9SZW1vdGVTdG9yYWdlSGVscGVyLmphdmE=) | `63.93% <0.00%> (-0.59%)` | `9.00% <0.00%> (ø%)` | |
| [...main/java/com/google/cloud/storage/BucketInfo.java](https://codecov.io/gh/googleapis/java-storage/pull/431/diff?src=pr&el=tree#diff-Z29vZ2xlLWNsb3VkLXN0b3JhZ2Uvc3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3N0b3JhZ2UvQnVja2V0SW5mby5qYXZh) | `81.37% <0.00%> (-0.20%)` | `83.00% <0.00%> (ø%)` | |
| [...ain/java/com/google/cloud/storage/StorageImpl.java](https://codecov.io/gh/googleapis/java-storage/pull/431/diff?src=pr&el=tree#diff-Z29vZ2xlLWNsb3VkLXN0b3JhZ2Uvc3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3N0b3JhZ2UvU3RvcmFnZUltcGwuamF2YQ==) | `80.86% <0.00%> (-0.14%)` | `138.00% <0.00%> (ø%)` | |
| [...e/src/main/java/com/google/cloud/storage/Blob.java](https://codecov.io/gh/googleapis/java-storage/pull/431/diff?src=pr&el=tree#diff-Z29vZ2xlLWNsb3VkLXN0b3JhZ2Uvc3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3N0b3JhZ2UvQmxvYi5qYXZh) | `82.08% <0.00%> (-0.11%)` | `29.00% <0.00%> (-1.00%)` | |
| [...om/google/cloud/storage/spi/v1/HttpStorageRpc.java](https://codecov.io/gh/googleapis/java-storage/pull/431/diff?src=pr&el=tree#diff-Z29vZ2xlLWNsb3VkLXN0b3JhZ2Uvc3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3N0b3JhZ2Uvc3BpL3YxL0h0dHBTdG9yYWdlUnBjLmphdmE=) | `1.50% <0.00%> (+<0.01%)` | `1.00% <0.00%> (ø%)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/googleapis/java-storage/pull/431?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/googleapis/java-storage/pull/431?src=pr&el=footer). Last update [12bd62c...a043ace](https://codecov.io/gh/googleapis/java-storage/pull/431?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 196a6653..ffacd705 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -2,7 +2,7 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-storage</artifactId>
-  <version>1.111.2</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>1.111.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>jar</packaging>
   <name>Google Cloud Storage</name>
   <url>https://github.com/googleapis/java-storage</url>
@@ -12,7 +12,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>1.111.2</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>1.111.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
@@ -33,7 +33,7 @@
     <dependency>
       <groupId>com.google.api-client</groupId>
       <artifactId>google-api-client</artifactId>
-      <version>1.30.9</version>
+      <version>1.30.10</version>
     </dependency>
     <dependency>
       <groupId>com.google.apis</groupId>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java
index df2936dc..cc9b413a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java
@@ -31,8 +31,8 @@ import java.util.Set;
  * @see <a href="https://cloud.google.com/storage/docs/xml-api/post-object">POST Object</a>
  */
 public final class PostPolicyV4 {
-  private String url;
-  private Map<String, String> fields;
+  private final String url;
+  private final Map<String, String> fields;
 
   private PostPolicyV4(String url, Map<String, String> fields) {
     this.url = url;
@@ -58,7 +58,7 @@ public final class PostPolicyV4 {
    *     Object Form fields</a>
    */
   public static final class PostFieldsV4 {
-    private Map<String, String> fieldsMap;
+    private final Map<String, String> fieldsMap;
 
     private PostFieldsV4(Builder builder) {
       this.fieldsMap = builder.fieldsMap;
@@ -81,10 +81,11 @@ public final class PostPolicyV4 {
     }
 
     public static class Builder {
-      private Map<String, String> fieldsMap;
+      private static final String CUSTOM_FIELD_PREFIX = "x-goog-meta-";
+      private final Map<String, String> fieldsMap;
 
       private Builder() {
-        fieldsMap = new HashMap<>();
+        this.fieldsMap = new HashMap<>();
       }
 
       public PostFieldsV4 build() {
@@ -121,7 +122,13 @@ public final class PostPolicyV4 {
         return this;
       }
 
+      /** @deprecated use {@link #setExpires(String)} */
+      @Deprecated
       public Builder Expires(String expires) {
+        return setExpires(expires);
+      }
+
+      public Builder setExpires(String expires) {
         fieldsMap.put("expires", expires);
         return this;
       }
@@ -136,8 +143,17 @@ public final class PostPolicyV4 {
         return this;
       }
 
+      /** @deprecated use {@link #setCustomMetadataField(String, String)} */
+      @Deprecated
       public Builder AddCustomMetadataField(String field, String value) {
-        fieldsMap.put("x-goog-meta-" + field, value);
+        return setCustomMetadataField(field, value);
+      }
+
+      public Builder setCustomMetadataField(String field, String value) {
+        if (!field.startsWith(CUSTOM_FIELD_PREFIX)) {
+          field = CUSTOM_FIELD_PREFIX + value;
+        }
+        fieldsMap.put(field, value);
         return this;
       }
     }
@@ -270,8 +286,8 @@ public final class PostPolicyV4 {
    *     Policy document</a>
    */
   public static final class PostPolicyV4Document {
-    private String expiration;
-    private PostConditionsV4 conditions;
+    private final String expiration;
+    private final PostConditionsV4 conditions;
 
     private PostPolicyV4Document(String expiration, PostConditionsV4 conditions) {
       this.expiration = expiration;
@@ -363,9 +379,9 @@ public final class PostPolicyV4 {
    *     Policy document</a>
    */
   static final class ConditionV4 {
-    ConditionV4Type type;
-    String operand1;
-    String operand2;
+    final ConditionV4Type type;
+    final String operand1;
+    final String operand2;
 
     private ConditionV4(ConditionV4Type type, String operand1, String operand2) {
       this.type = type;
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index eb15dd37..202615cd 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
@@ -521,6 +521,15 @@ public interface Storage extends Service<StorageOptions> {
       return new BlobTargetOption(StorageRpc.Option.IF_DISABLE_GZIP_CONTENT, true);
     }
 
+    /**
+     * Returns an option for detecting content type. If this option is used, the content type is
+     * detected from the blob name if not explicitly set. This option is on the client side only, it
+     * does not appear in a RPC call.
+     */
+    public static BlobTargetOption detectContentType() {
+      return new BlobTargetOption(StorageRpc.Option.DETECT_CONTENT_TYPE, true);
+    }
+
     /**
      * Returns an option to set a customer-supplied AES256 key for server-side encryption of the
      * blob.
@@ -593,6 +602,7 @@ public interface Storage extends Service<StorageOptions> {
       CUSTOMER_SUPPLIED_KEY,
       KMS_KEY_NAME,
       USER_PROJECT,
+      DETECT_CONTENT_TYPE,
       IF_DISABLE_GZIP_CONTENT;
 
       StorageRpc.Option toRpcOption() {
@@ -733,6 +743,15 @@ public interface Storage extends Service<StorageOptions> {
     public static BlobWriteOption disableGzipContent() {
       return new BlobWriteOption(Option.IF_DISABLE_GZIP_CONTENT, true);
     }
+
+    /**
+     * Returns an option for detecting content type. If this option is used, the content type is
+     * detected from the blob name if not explicitly set. This option is on the client side only, it
+     * does not appear in a RPC call.
+     */
+    public static BlobWriteOption detectContentType() {
+      return new BlobWriteOption(Option.DETECT_CONTENT_TYPE, true);
+    }
   }
 
   /** Class for specifying blob source options. */
@@ -1832,9 +1851,10 @@ public interface Storage extends Service<StorageOptions> {
    * Creates a new blob. Direct upload is used to upload {@code content}. For large content, {@link
    * #writer} is recommended as it uses resumable upload. MD5 and CRC32C hashes of {@code content}
    * are computed and used for validating transferred data. Accepts an optional userProject {@link
-   * BlobGetOption} option which defines the project id to assign operational costs.
+   * BlobGetOption} option which defines the project id to assign operational costs. The content
+   * type is detected from the blob name if not explicitly set.
    *
-   * <p>Example of creating a blob from a byte array.
+   * <p>Example of creating a blob from a byte array:
    *
    * <pre>{@code
    * String bucketName = "my-unique-bucket";
@@ -1857,7 +1877,7 @@ public interface Storage extends Service<StorageOptions> {
    * Accepts a userProject {@link BlobGetOption} option, which defines the project id to assign
    * operational costs.
    *
-   * <p>Example of creating a blob from a byte array.
+   * <p>Example of creating a blob from a byte array:
    *
    * <pre>{@code
    * String bucketName = "my-unique-bucket";
@@ -1876,7 +1896,7 @@ public interface Storage extends Service<StorageOptions> {
 
   /**
    * Creates a new blob. Direct upload is used to upload {@code content}. For large content, {@link
-   * #writer} is recommended as it uses resumable upload. By default any md5 and crc32c values in
+   * #writer} is recommended as it uses resumable upload. By default any MD5 and CRC32C values in
    * the given {@code blobInfo} are ignored unless requested via the {@code
    * BlobWriteOption.md5Match} and {@code BlobWriteOption.crc32cMatch} options. The given input
    * stream is closed upon success.
@@ -2603,11 +2623,11 @@ public interface Storage extends Service<StorageOptions> {
   ReadChannel reader(BlobId blob, BlobSourceOption... options);
 
   /**
-   * Creates a blob and return a channel for writing its content. By default any md5 and crc32c
+   * Creates a blob and returns a channel for writing its content. By default any MD5 and CRC32C
    * values in the given {@code blobInfo} are ignored unless requested via the {@code
    * BlobWriteOption.md5Match} and {@code BlobWriteOption.crc32cMatch} options.
    *
-   * <p>Example of writing a blob's content through a writer.
+   * <p>Example of writing a blob's content through a writer:
    *
    * <pre>{@code
    * String bucketName = "my-unique-bucket";
@@ -3517,7 +3537,7 @@ public interface Storage extends Service<StorageOptions> {
    * String bucketName = "my-unique-bucket";
    * List<Boolean> response =
    *     storage.testIamPermissions(
-   *         bucket,
+   *         bucketName,
    *         ImmutableList.of("storage.buckets.get", "storage.buckets.getIamPolicy"));
    * for (boolean hasPermission : response) {
    *   // Do something with permission test response
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
index 519a9e9f..ec849281 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
@@ -77,9 +77,12 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.math.BigInteger;
+import java.net.FileNameMap;
+import java.net.URLConnection;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 public class HttpStorageRpc implements StorageRpc {
@@ -98,6 +101,7 @@ public class HttpStorageRpc implements StorageRpc {
   private final HttpRequestInitializer batchRequestInitializer;
 
   private static final long MEGABYTE = 1024L * 1024L;
+  private static final FileNameMap FILE_NAME_MAP = URLConnection.getFileNameMap();
 
   public HttpStorageRpc(StorageOptions options) {
     HttpTransportOptions transportOptions = (HttpTransportOptions) options.getTransportOptions();
@@ -286,7 +290,7 @@ public class HttpStorageRpc implements StorageRpc {
               .insert(
                   storageObject.getBucket(),
                   storageObject,
-                  new InputStreamContent(storageObject.getContentType(), content));
+                  new InputStreamContent(detectContentType(storageObject, options), content));
       insert.getMediaHttpUploader().setDirectUploadEnabled(true);
       Boolean disableGzipContent = Option.IF_DISABLE_GZIP_CONTENT.getBoolean(options);
       if (disableGzipContent != null) {
@@ -372,6 +376,19 @@ public class HttpStorageRpc implements StorageRpc {
     }
   }
 
+  private static String detectContentType(StorageObject object, Map<Option, ?> options) {
+    String contentType = object.getContentType();
+    if (contentType != null) {
+      return contentType;
+    }
+
+    if (Boolean.TRUE == Option.DETECT_CONTENT_TYPE.get(options)) {
+      contentType = FILE_NAME_MAP.getContentTypeFor(object.getName().toLowerCase(Locale.ENGLISH));
+    }
+
+    return firstNonNull(contentType, "application/octet-stream");
+  }
+
   private static Function<String, StorageObject> objectFromPrefix(final String bucket) {
     return new Function<String, StorageObject>() {
       @Override
@@ -834,9 +851,7 @@ public class HttpStorageRpc implements StorageRpc {
       HttpRequest httpRequest =
           requestFactory.buildPostRequest(url, new JsonHttpContent(jsonFactory, object));
       HttpHeaders requestHeaders = httpRequest.getHeaders();
-      requestHeaders.set(
-          "X-Upload-Content-Type",
-          firstNonNull(object.getContentType(), "application/octet-stream"));
+      requestHeaders.set("X-Upload-Content-Type", detectContentType(object, options));
       String key = Option.CUSTOMER_SUPPLIED_KEY.getString(options);
       if (key != null) {
         BaseEncoding base64 = BaseEncoding.base64();
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java
index 7ae9c8ec..d1249f6d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java
@@ -65,7 +65,8 @@ public interface StorageRpc extends ServiceRpc {
     KMS_KEY_NAME("kmsKeyName"),
     SERVICE_ACCOUNT_EMAIL("serviceAccount"),
     SHOW_DELETED_KEYS("showDeletedKeys"),
-    REQUESTED_POLICY_VERSION("optionsRequestedPolicyVersion");
+    REQUESTED_POLICY_VERSION("optionsRequestedPolicyVersion"),
+    DETECT_CONTENT_TYPE("detectContentType");
 
     private final String value;
 
diff --git a/pom.xml b/pom.xml
index 40d8e296..853bd01e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-parent</artifactId>
   <packaging>pom</packaging>
-  <version>1.111.2</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>1.111.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -78,7 +78,7 @@
       <dependency>
         <groupId>com.google.apis</groupId>
         <artifactId>google-api-services-storage</artifactId>
-        <version>v1-rev20200611-1.30.9</version>
+        <version>v1-rev20200611-1.30.10</version>
       </dependency>
       <dependency>
         <groupId>org.easymock</groupId>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index cbe71fe4..4dad481d 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -29,7 +29,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>1.111.1</version>
+      <version>1.111.2</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index cad01e09..f662ee16 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>1.111.1</version>
+      <version>1.111.2</version>
     </dependency>
 
     <dependency>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 320dca50..e1a72331 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -30,7 +30,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>8.0.0</version>
+        <version>8.1.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/versions.txt b/versions.txt
index 2eee4166..ef36ef92 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:1.111.2:1.111.2
\ No newline at end of file
+google-cloud-storage:1.111.2:1.111.3-SNAPSHOT
\ No newline at end of file


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
index ba4b3623..fbce81db 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
@@ -3363,4 +3363,61 @@ public class ITStorageTest {
     byte[] readBytes = blob.getContent(Blob.BlobSourceOption.decryptionKey(KEY));
     assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
   }
+
+  private Blob createBlob(String method, BlobInfo blobInfo, boolean detectType) throws IOException {
+    switch (method) {
+      case "create":
+        return detectType
+            ? storage.create(blobInfo, Storage.BlobTargetOption.detectContentType())
+            : storage.create(blobInfo);
+      case "createFrom":
+        InputStream inputStream = new ByteArrayInputStream(BLOB_BYTE_CONTENT);
+        return detectType
+            ? storage.createFrom(blobInfo, inputStream, Storage.BlobWriteOption.detectContentType())
+            : storage.createFrom(blobInfo, inputStream);
+      case "writer":
+        if (detectType) {
+          storage.writer(blobInfo, Storage.BlobWriteOption.detectContentType()).close();
+        } else {
+          storage.writer(blobInfo).close();
+        }
+        return storage.get(BlobId.of(blobInfo.getBucket(), blobInfo.getName()));
+      default:
+        throw new IllegalArgumentException("Unknown method " + method);
+    }
+  }
+
+  private void testAutoContentType(String method) throws IOException {
+    String[] names = {"file1.txt", "dir with spaces/Pic.Jpg", "no_extension"};
+    String[] types = {"text/plain", "image/jpeg", "application/octet-stream"};
+    for (int i = 0; i < names.length; i++) {
+      BlobId blobId = BlobId.of(BUCKET, names[i]);
+      BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
+      Blob blob_true = createBlob(method, blobInfo, true);
+      assertEquals(types[i], blob_true.getContentType());
+
+      Blob blob_false = createBlob(method, blobInfo, false);
+      assertEquals("application/octet-stream", blob_false.getContentType());
+    }
+    String customType = "custom/type";
+    BlobId blobId = BlobId.of(BUCKET, names[0]);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType(customType).build();
+    Blob blob = createBlob(method, blobInfo, true);
+    assertEquals(customType, blob.getContentType());
+  }
+
+  @Test
+  public void testAutoContentTypeCreate() throws IOException {
+    testAutoContentType("create");
+  }
+
+  @Test
+  public void testAutoContentTypeCreateFrom() throws IOException {
+    testAutoContentType("createFrom");
+  }
+
+  @Test
+  public void testAutoContentTypeWriter() throws IOException {
+    testAutoContentType("writer");
+  }
 }