Skip to content

Commit 66d1eb7

Browse files
authored
feat: auto content-type on blob creation (#338)
* feat: auto content-type on blob creation * feat: auto content-type on blob creation * feat: auto content-type on blob creation * feat: auto content-type on blob creation * feat: auto content-type on blob creation * feat: auto content-type on blob creation * detection content type is made optional
1 parent 43202f6 commit 66d1eb7

File tree

4 files changed

+104
-11
lines changed

4 files changed

+104
-11
lines changed

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,15 @@ public static BlobTargetOption disableGzipContent() {
521521
return new BlobTargetOption(StorageRpc.Option.IF_DISABLE_GZIP_CONTENT, true);
522522
}
523523

524+
/**
525+
* Returns an option for detecting content type. If this option is used, the content type is
526+
* detected from the blob name if not explicitly set. This option is on the client side only, it
527+
* does not appear in a RPC call.
528+
*/
529+
public static BlobTargetOption detectContentType() {
530+
return new BlobTargetOption(StorageRpc.Option.DETECT_CONTENT_TYPE, true);
531+
}
532+
524533
/**
525534
* Returns an option to set a customer-supplied AES256 key for server-side encryption of the
526535
* blob.
@@ -593,6 +602,7 @@ enum Option {
593602
CUSTOMER_SUPPLIED_KEY,
594603
KMS_KEY_NAME,
595604
USER_PROJECT,
605+
DETECT_CONTENT_TYPE,
596606
IF_DISABLE_GZIP_CONTENT;
597607

598608
StorageRpc.Option toRpcOption() {
@@ -733,6 +743,15 @@ public static BlobWriteOption userProject(String userProject) {
733743
public static BlobWriteOption disableGzipContent() {
734744
return new BlobWriteOption(Option.IF_DISABLE_GZIP_CONTENT, true);
735745
}
746+
747+
/**
748+
* Returns an option for detecting content type. If this option is used, the content type is
749+
* detected from the blob name if not explicitly set. This option is on the client side only, it
750+
* does not appear in a RPC call.
751+
*/
752+
public static BlobWriteOption detectContentType() {
753+
return new BlobWriteOption(Option.DETECT_CONTENT_TYPE, true);
754+
}
736755
}
737756

738757
/** Class for specifying blob source options. */
@@ -1832,9 +1851,10 @@ public static Builder newBuilder() {
18321851
* Creates a new blob. Direct upload is used to upload {@code content}. For large content, {@link
18331852
* #writer} is recommended as it uses resumable upload. MD5 and CRC32C hashes of {@code content}
18341853
* are computed and used for validating transferred data. Accepts an optional userProject {@link
1835-
* BlobGetOption} option which defines the project id to assign operational costs.
1854+
* BlobGetOption} option which defines the project id to assign operational costs. The content
1855+
* type is detected from the blob name if not explicitly set.
18361856
*
1837-
* <p>Example of creating a blob from a byte array.
1857+
* <p>Example of creating a blob from a byte array:
18381858
*
18391859
* <pre>{@code
18401860
* String bucketName = "my-unique-bucket";
@@ -1857,7 +1877,7 @@ public static Builder newBuilder() {
18571877
* Accepts a userProject {@link BlobGetOption} option, which defines the project id to assign
18581878
* operational costs.
18591879
*
1860-
* <p>Example of creating a blob from a byte array.
1880+
* <p>Example of creating a blob from a byte array:
18611881
*
18621882
* <pre>{@code
18631883
* String bucketName = "my-unique-bucket";
@@ -1876,7 +1896,7 @@ Blob create(
18761896

18771897
/**
18781898
* Creates a new blob. Direct upload is used to upload {@code content}. For large content, {@link
1879-
* #writer} is recommended as it uses resumable upload. By default any md5 and crc32c values in
1899+
* #writer} is recommended as it uses resumable upload. By default any MD5 and CRC32C values in
18801900
* the given {@code blobInfo} are ignored unless requested via the {@code
18811901
* BlobWriteOption.md5Match} and {@code BlobWriteOption.crc32cMatch} options. The given input
18821902
* stream is closed upon success.
@@ -2603,11 +2623,11 @@ Blob createFrom(
26032623
ReadChannel reader(BlobId blob, BlobSourceOption... options);
26042624

26052625
/**
2606-
* Creates a blob and return a channel for writing its content. By default any md5 and crc32c
2626+
* Creates a blob and returns a channel for writing its content. By default any MD5 and CRC32C
26072627
* values in the given {@code blobInfo} are ignored unless requested via the {@code
26082628
* BlobWriteOption.md5Match} and {@code BlobWriteOption.crc32cMatch} options.
26092629
*
2610-
* <p>Example of writing a blob's content through a writer.
2630+
* <p>Example of writing a blob's content through a writer:
26112631
*
26122632
* <pre>{@code
26132633
* String bucketName = "my-unique-bucket";

google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@
7777
import java.io.InputStream;
7878
import java.io.OutputStream;
7979
import java.math.BigInteger;
80+
import java.net.FileNameMap;
81+
import java.net.URLConnection;
8082
import java.util.ArrayList;
8183
import java.util.LinkedList;
8284
import java.util.List;
85+
import java.util.Locale;
8386
import java.util.Map;
8487

8588
public class HttpStorageRpc implements StorageRpc {
@@ -98,6 +101,7 @@ public class HttpStorageRpc implements StorageRpc {
98101
private final HttpRequestInitializer batchRequestInitializer;
99102

100103
private static final long MEGABYTE = 1024L * 1024L;
104+
private static final FileNameMap FILE_NAME_MAP = URLConnection.getFileNameMap();
101105

102106
public HttpStorageRpc(StorageOptions options) {
103107
HttpTransportOptions transportOptions = (HttpTransportOptions) options.getTransportOptions();
@@ -286,7 +290,7 @@ public StorageObject create(
286290
.insert(
287291
storageObject.getBucket(),
288292
storageObject,
289-
new InputStreamContent(storageObject.getContentType(), content));
293+
new InputStreamContent(detectContentType(storageObject, options), content));
290294
insert.getMediaHttpUploader().setDirectUploadEnabled(true);
291295
Boolean disableGzipContent = Option.IF_DISABLE_GZIP_CONTENT.getBoolean(options);
292296
if (disableGzipContent != null) {
@@ -372,6 +376,19 @@ public Tuple<String, Iterable<StorageObject>> list(final String bucket, Map<Opti
372376
}
373377
}
374378

379+
private static String detectContentType(StorageObject object, Map<Option, ?> options) {
380+
String contentType = object.getContentType();
381+
if (contentType != null) {
382+
return contentType;
383+
}
384+
385+
if (Boolean.TRUE == Option.DETECT_CONTENT_TYPE.get(options)) {
386+
contentType = FILE_NAME_MAP.getContentTypeFor(object.getName().toLowerCase(Locale.ENGLISH));
387+
}
388+
389+
return firstNonNull(contentType, "application/octet-stream");
390+
}
391+
375392
private static Function<String, StorageObject> objectFromPrefix(final String bucket) {
376393
return new Function<String, StorageObject>() {
377394
@Override
@@ -834,9 +851,7 @@ public String open(StorageObject object, Map<Option, ?> options) {
834851
HttpRequest httpRequest =
835852
requestFactory.buildPostRequest(url, new JsonHttpContent(jsonFactory, object));
836853
HttpHeaders requestHeaders = httpRequest.getHeaders();
837-
requestHeaders.set(
838-
"X-Upload-Content-Type",
839-
firstNonNull(object.getContentType(), "application/octet-stream"));
854+
requestHeaders.set("X-Upload-Content-Type", detectContentType(object, options));
840855
String key = Option.CUSTOMER_SUPPLIED_KEY.getString(options);
841856
if (key != null) {
842857
BaseEncoding base64 = BaseEncoding.base64();

google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ enum Option {
6565
KMS_KEY_NAME("kmsKeyName"),
6666
SERVICE_ACCOUNT_EMAIL("serviceAccount"),
6767
SHOW_DELETED_KEYS("showDeletedKeys"),
68-
REQUESTED_POLICY_VERSION("optionsRequestedPolicyVersion");
68+
REQUESTED_POLICY_VERSION("optionsRequestedPolicyVersion"),
69+
DETECT_CONTENT_TYPE("detectContentType");
6970

7071
private final String value;
7172

google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3363,4 +3363,61 @@ public void testUploadWithEncryption() throws Exception {
33633363
byte[] readBytes = blob.getContent(Blob.BlobSourceOption.decryptionKey(KEY));
33643364
assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
33653365
}
3366+
3367+
private Blob createBlob(String method, BlobInfo blobInfo, boolean detectType) throws IOException {
3368+
switch (method) {
3369+
case "create":
3370+
return detectType
3371+
? storage.create(blobInfo, Storage.BlobTargetOption.detectContentType())
3372+
: storage.create(blobInfo);
3373+
case "createFrom":
3374+
InputStream inputStream = new ByteArrayInputStream(BLOB_BYTE_CONTENT);
3375+
return detectType
3376+
? storage.createFrom(blobInfo, inputStream, Storage.BlobWriteOption.detectContentType())
3377+
: storage.createFrom(blobInfo, inputStream);
3378+
case "writer":
3379+
if (detectType) {
3380+
storage.writer(blobInfo, Storage.BlobWriteOption.detectContentType()).close();
3381+
} else {
3382+
storage.writer(blobInfo).close();
3383+
}
3384+
return storage.get(BlobId.of(blobInfo.getBucket(), blobInfo.getName()));
3385+
default:
3386+
throw new IllegalArgumentException("Unknown method " + method);
3387+
}
3388+
}
3389+
3390+
private void testAutoContentType(String method) throws IOException {
3391+
String[] names = {"file1.txt", "dir with spaces/Pic.Jpg", "no_extension"};
3392+
String[] types = {"text/plain", "image/jpeg", "application/octet-stream"};
3393+
for (int i = 0; i < names.length; i++) {
3394+
BlobId blobId = BlobId.of(BUCKET, names[i]);
3395+
BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
3396+
Blob blob_true = createBlob(method, blobInfo, true);
3397+
assertEquals(types[i], blob_true.getContentType());
3398+
3399+
Blob blob_false = createBlob(method, blobInfo, false);
3400+
assertEquals("application/octet-stream", blob_false.getContentType());
3401+
}
3402+
String customType = "custom/type";
3403+
BlobId blobId = BlobId.of(BUCKET, names[0]);
3404+
BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType(customType).build();
3405+
Blob blob = createBlob(method, blobInfo, true);
3406+
assertEquals(customType, blob.getContentType());
3407+
}
3408+
3409+
@Test
3410+
public void testAutoContentTypeCreate() throws IOException {
3411+
testAutoContentType("create");
3412+
}
3413+
3414+
@Test
3415+
public void testAutoContentTypeCreateFrom() throws IOException {
3416+
testAutoContentType("createFrom");
3417+
}
3418+
3419+
@Test
3420+
public void testAutoContentTypeWriter() throws IOException {
3421+
testAutoContentType("writer");
3422+
}
33663423
}

0 commit comments

Comments
 (0)