Repository: googleapis/java-storage (java)

Issue:
Add Blob.uploadFrom(InputStream)
We have [Blob.downloadTo(OutputStream)](https://googleapis.dev/java/google-cloud-clients/latest/com/google/cloud/storage/Blob.html#downloadTo-java.io.OutputStream-com.google.cloud.storage.Blob.BlobSourceOption...-) (and other downloadFrom variants)

Should probably have uploadFrom with a stream. People currently have to buffer/chunk their writes [manually](https://googleapis.dev/java/google-cloud-clients/latest/com/google/cloud/storage/Blob.html#writer-com.google.cloud.storage.Storage.BlobWriteOption...-).

/cc @crwilcox 

Pull request description:
feat: add blob.uploadfrom(inputstream)
Fixes #62 

Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 0bd0ee06..330f81ef 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,1 +1,7 @@
-Fixes #<issue_number_goes_here> (it's a good idea to open an issue first for context and/or discussion)
\ No newline at end of file
+Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
+- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/java-storage/issues/new/choose) before writing your code!  That way we can discuss the change, evaluate designs, and agree on the general idea
+- [ ] Ensure the tests and linter pass
+- [ ] Code coverage does not decrease (if any source code was changed)
+- [ ] Appropriate docs were updated (if necessary)
+
+Fixes #<issue_number_goes_here> ☕️
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6b91f98..5726d249 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 # Changelog
 
+### [1.105.2](https://www.github.com/googleapis/java-storage/compare/v1.105.1...v1.105.2) (2020-03-13)
+
+
+### Bug Fixes
+
+* connection closed prematurely in BlobReadChannel & ConnectionReset ([#173](https://www.github.com/googleapis/java-storage/issues/173)) ([27bccda](https://www.github.com/googleapis/java-storage/commit/27bccda384da4a7b877b371fbaecc794d6304fbf))
+
+
+### Dependencies
+
+* update core dependencies ([#171](https://www.github.com/googleapis/java-storage/issues/171)) ([ef5f2c6](https://www.github.com/googleapis/java-storage/commit/ef5f2c6e5079debe8f7f37c3d2c501aac3dc82a6))
+
 ### [1.105.1](https://www.github.com/googleapis/java-storage/compare/v1.105.0...v1.105.1) (2020-03-09)
 
 
diff --git a/README.md b/README.md
index c750e5cd..e967ae7f 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ If you are using Maven without Bom, Add this to your dependencies.
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage</artifactId>
-  <version>1.105.1</version>
+  <version>1.105.2</version>
 </dependency>
 ```
 If you are using Gradle, add this to your dependencies
@@ -48,11 +48,11 @@ repositories {
         //other repositories
         }
 
-compile 'com.google.cloud:google-cloud-storage:1.105.1'
+compile 'com.google.cloud:google-cloud-storage:1.105.2'
 ```
 If you are using SBT, add this to your dependencies
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "1.105.1"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "1.105.2"
 ```
 [//]: # ({x-version-update-end})
 
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 84ca0fc1..8ab2ba8a 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.105.1</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>1.105.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.105.1</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>1.105.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
index 27e62626..ccd7315f 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
@@ -38,9 +38,11 @@ import com.google.common.base.Function;
 import com.google.common.io.BaseEncoding;
 import com.google.common.io.CountingOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.ObjectInputStream;
 import java.io.OutputStream;
 import java.net.URL;
+import java.nio.ByteBuffer;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.security.Key;
@@ -73,7 +75,8 @@ public class Blob extends BlobInfo {
         }
       };
 
-  private static final int DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024;
+  private static final int DEFAULT_CHUNK_SIZE = 15 * 1024 * 1024;
+  private static final int MIN_BUFFER_SIZE = 256 * 1024;
 
   /** Class for specifying blob source options when {@code Blob} methods are used. */
   public static class BlobSourceOption extends Option {
@@ -260,6 +263,88 @@ public class Blob extends BlobInfo {
     downloadTo(path, new BlobSourceOption[0]);
   }
 
+  /**
+   * Uploads the given file path to this blob using specified blob write options.
+   *
+   * @param path file to upload
+   * @param options blob write options
+   * @return updated blob
+   * @throws IOException on I/O error
+   * @throws StorageException on failure
+   */
+  public Blob uploadFrom(Path path, BlobWriteOption... options) throws IOException {
+    if (Files.isDirectory(path)) {
+      throw new StorageException(0, path + " is a directory");
+    }
+    try (InputStream input = Files.newInputStream(path)) {
+      return uploadFrom(input, options);
+    }
+  }
+
+  /**
+   * Uploads the given content to this blob using specified blob write options.
+   *
+   * @param input content to upload
+   * @param options blob write options
+   * @return updated blob
+   * @throws IOException on I/O error
+   * @throws StorageException on failure
+   */
+  public Blob uploadFrom(InputStream input, BlobWriteOption... options) throws IOException {
+    try (WriteChannel writer = storage.writer(this, options)) {
+      uploadFrom(input, writer);
+    }
+    BlobId blobId = getBlobId();
+    try {
+      return storage.get(BlobId.of(blobId.getBucket(), blobId.getName()));
+    } catch (StorageException e) {
+      throw new StorageException(
+          e.getCode(), "Content has been uploaded successfully. Failed to retrieve blob.", e);
+    }
+  }
+
+  static void uploadFrom(InputStream input, WriteChannel writer) throws IOException {
+    uploadFrom(input, writer, DEFAULT_CHUNK_SIZE);
+  }
+
+  /**
+   * Uploads the given content to the storage using specified write channel and the given buffer
+   * size. Other uploadFrom() methods invoke this one with a buffer size of 15 MiB. Users can pass
+   * alternative values. Larger buffer sizes might improve the upload performance but require more
+   * memory. This can cause an OutOfMemoryError or add significant garbage collection overhead.
+   * Smaller buffer sizes reduce memory consumption, that is noticeable when uploading many objects
+   * in parallel. Buffer sizes less than 256 KiB are treated as 256 KiB.
+   *
+   * <p>This method does not close either the InputStream or the WriterChannel.
+   *
+   * <p>Example of uploading:
+   *
+   * <pre>{@code
+   * BlobId blobId = BlobId.of(bucketName, blobName);
+   * BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType("video/webm").build();
+   * Path file = Paths.get("humongous.file");
+   * try (InputStream input = Files.newInputStream(file); WriteChannel writer = storage.writer(blobInfo)) {
+   *   Blob.uploadFrom(input, writer, 150 * 1024 * 1024);
+   * } catch (IOException e) {
+   *   // your handler
+   * }
+   * }</pre>
+   *
+   * @param input content to upload
+   * @param writer channel
+   * @param bufferSize size of the buffer to read from input and send over writer
+   * @throws IOException on I/O error
+   */
+  public static void uploadFrom(InputStream input, WriteChannel writer, int bufferSize)
+      throws IOException {
+    bufferSize = Math.max(bufferSize, MIN_BUFFER_SIZE);
+    byte[] buffer = new byte[bufferSize];
+    int length;
+    while ((length = input.read(buffer)) >= 0) {
+      writer.write(ByteBuffer.wrap(buffer, 0, length));
+    }
+  }
+
   /** Builder for {@code Blob}. */
   public static class Builder extends BlobInfo.Builder {
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java
index 01153a35..1f740194 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java
@@ -33,6 +33,9 @@ import java.util.Set;
  */
 @InternalApi
 public final class StorageException extends BaseHttpServiceException {
+  private static final String INTERNAL_ERROR = "internalError";
+  private static final String CONNECTION_CLOSED_PREMATURELY = "connectionClosedPrematurely";
+  private static final String CONNECTION_RESET = "connectionReset";
 
   // see: https://cloud.google.com/storage/docs/resumable-uploads-xml#practices
   private static final Set<Error> RETRYABLE_ERRORS =
@@ -43,7 +46,9 @@ public final class StorageException extends BaseHttpServiceException {
           new Error(500, null),
           new Error(429, null),
           new Error(408, null),
-          new Error(null, "internalError"));
+          new Error(null, INTERNAL_ERROR),
+          new Error(null, CONNECTION_CLOSED_PREMATURELY),
+          new Error(null, CONNECTION_RESET));
 
   private static final long serialVersionUID = -4168430271327813063L;
 
@@ -55,6 +60,10 @@ public final class StorageException extends BaseHttpServiceException {
     super(code, message, null, true, RETRYABLE_ERRORS, cause);
   }
 
+  public StorageException(int code, String message, String reason, Throwable cause) {
+    super(code, message, reason, true, RETRYABLE_ERRORS, cause);
+  }
+
   public StorageException(IOException exception) {
     super(exception, true, RETRYABLE_ERRORS);
   }
@@ -73,4 +82,23 @@ public final class StorageException extends BaseHttpServiceException {
     BaseServiceException.translate(ex);
     throw new StorageException(UNKNOWN_CODE, ex.getMessage(), ex.getCause());
   }
+
+  /**
+   * Translate IOException to a StorageException representing the cause of the error. This method
+   * defaults to idempotent always being {@code true}. Additionally, this method translates
+   * transient issues Connection Closed Prematurely and Connection Reset as retryable errors.
+   *
+   * @returns {@code StorageException}
+   */
+  public static StorageException translate(IOException exception) {
+    if (exception.getMessage().contains("Connection closed prematurely")) {
+      return new StorageException(
+          0, exception.getMessage(), CONNECTION_CLOSED_PREMATURELY, exception);
+    } else if (exception.getMessage().contains("Connection reset")) {
+      return new StorageException(0, exception.getMessage(), CONNECTION_RESET, exception);
+    } else {
+      // default
+      return new StorageException(exception);
+    }
+  }
 }
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 5a6e947a..c677cbdb 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
@@ -701,7 +701,7 @@ public class HttpStorageRpc implements StorageRpc {
       return Tuple.of(etag, output.toByteArray());
     } catch (IOException ex) {
       span.setStatus(Status.UNKNOWN.withDescription(ex.getMessage()));
-      StorageException serviceException = translate(ex);
+      StorageException serviceException = StorageException.translate(ex);
       if (serviceException.getCode() == SC_REQUESTED_RANGE_NOT_SATISFIABLE) {
         return Tuple.of(null, new byte[0]);
       }
diff --git a/pom.xml b/pom.xml
index 802908a9..46619c8a 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.105.1</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>1.105.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -103,7 +103,7 @@
       <dependency>
         <groupId>io.grpc</groupId>
         <artifactId>grpc-bom</artifactId>
-        <version>1.28.0</version>
+        <version>1.27.2</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/synth.metadata b/synth.metadata
index 00adeb28..b8dc069d 100644
--- a/synth.metadata
+++ b/synth.metadata
@@ -1,5 +1,5 @@
 {
-  "updateTime": "2020-02-24T23:06:36.186663Z",
+  "updateTime": "2020-03-13T22:11:36.382819Z",
   "sources": [
     {
       "template": {
diff --git a/versions.txt b/versions.txt
index 3661bfee..86aaeafa 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:1.105.1:1.105.1
\ No newline at end of file
+google-cloud-storage:1.105.2:1.105.3-SNAPSHOT
\ No newline at end of file


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobReadChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobReadChannelTest.java
index 181e5414..f1388ebe 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobReadChannelTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobReadChannelTest.java
@@ -28,7 +28,6 @@ import static org.junit.Assert.fail;
 
 import com.google.cloud.ReadChannel;
 import com.google.cloud.RestorableState;
-import com.google.cloud.ServiceOptions;
 import com.google.cloud.Tuple;
 import com.google.cloud.storage.spi.StorageRpcFactory;
 import com.google.cloud.storage.spi.v1.StorageRpc;
@@ -68,7 +67,6 @@ public class BlobReadChannelTest {
         StorageOptions.newBuilder()
             .setProjectId("projectId")
             .setServiceRpcFactory(rpcFactoryMock)
-            .setRetrySettings(ServiceOptions.getNoRetrySettings())
             .build();
   }
 
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java
index 6f91f968..4c91c7db 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java
@@ -32,11 +32,13 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.google.api.core.ApiClock;
 import com.google.api.gax.retrying.RetrySettings;
 import com.google.api.services.storage.model.StorageObject;
 import com.google.cloud.ReadChannel;
+import com.google.cloud.WriteChannel;
 import com.google.cloud.storage.Acl.Project;
 import com.google.cloud.storage.Acl.Project.ProjectRole;
 import com.google.cloud.storage.Acl.Role;
@@ -48,10 +50,17 @@ import com.google.cloud.storage.spi.v1.StorageRpc;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.BaseEncoding;
+import java.io.ByteArrayInputStream;
 import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
+import java.nio.ByteBuffer;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.Key;
 import java.util.List;
 import java.util.Map;
@@ -586,7 +595,7 @@ public class BlobTest {
   }
 
   @Test
-  public void testDownload() throws Exception {
+  public void testDownloadTo() throws Exception {
     final byte[] expected = {1, 2};
     StorageRpc mockStorageRpc = createNiceMock(StorageRpc.class);
     expect(storage.getOptions()).andReturn(mockOptions).times(1);
@@ -618,7 +627,7 @@ public class BlobTest {
   }
 
   @Test
-  public void testDownloadWithRetries() throws Exception {
+  public void testDownloadToWithRetries() throws Exception {
     final byte[] expected = {1, 2};
     StorageRpc mockStorageRpc = createNiceMock(StorageRpc.class);
     expect(storage.getOptions()).andReturn(mockOptions);
@@ -662,4 +671,135 @@ public class BlobTest {
     byte actual[] = Files.readAllBytes(file.toPath());
     assertArrayEquals(expected, actual);
   }
+
+  @Test
+  public void testUploadFromNonExistentFile() {
+    initializeExpectedBlob(1);
+    expect(storage.getOptions()).andReturn(mockOptions);
+    replay(storage);
+    blob = new Blob(storage, new BlobInfo.BuilderImpl(BLOB_INFO));
+    String fileName = "non_existing_file.txt";
+    try {
+      blob.uploadFrom(Paths.get(fileName));
+      fail();
+    } catch (IOException e) {
+      assertEquals(NoSuchFileException.class, e.getClass());
+      assertEquals(fileName, e.getMessage());
+    }
+  }
+
+  @Test
+  public void testUploadFromDirectory() throws IOException {
+    initializeExpectedBlob(1);
+    expect(storage.getOptions()).andReturn(mockOptions);
+    replay(storage);
+    blob = new Blob(storage, new BlobInfo.BuilderImpl(BLOB_INFO));
+    Path dir = Files.createTempDirectory("unit_");
+    try {
+      blob.uploadFrom(dir);
+      fail();
+    } catch (StorageException e) {
+      assertEquals(dir + " is a directory", e.getMessage());
+    }
+  }
+
+  private WriteChannel createWriteChannelMock(byte[] bytes) throws Exception {
+    WriteChannel channel = createMock(WriteChannel.class);
+    ByteBuffer expectedByteBuffer = ByteBuffer.wrap(bytes, 0, bytes.length);
+    expect(channel.write(expectedByteBuffer)).andReturn(bytes.length);
+    channel.close();
+    replay(channel);
+    return channel;
+  }
+
+  private Blob createBlobForUpload(WriteChannel channel) {
+    initializeExpectedBlob(1);
+    BlobId blobId = BlobId.of(BLOB_INFO.getBucket(), BLOB_INFO.getName());
+    expect(storage.getOptions()).andReturn(mockOptions);
+    expect(storage.writer(eq(expectedBlob))).andReturn(channel);
+    expect(storage.get(blobId)).andReturn(expectedBlob);
+    replay(storage);
+    return new Blob(storage, new BlobInfo.BuilderImpl(BLOB_INFO));
+  }
+
+  @Test
+  public void testUploadFromFile() throws Exception {
+    byte[] dataToSend = {1, 2, 3};
+    WriteChannel channel = createWriteChannelMock(dataToSend);
+    blob = createBlobForUpload(channel);
+    Path tempFile = Files.createTempFile("testUpload", ".tmp");
+    Files.write(tempFile, dataToSend);
+    blob = blob.uploadFrom(tempFile);
+    assertSame(expectedBlob, blob);
+  }
+
+  @Test
+  public void testUploadFromStream() throws Exception {
+    byte[] dataToSend = {1, 2, 3, 4, 5};
+    WriteChannel channel = createWriteChannelMock(dataToSend);
+    blob = createBlobForUpload(channel);
+    InputStream input = new ByteArrayInputStream(dataToSend);
+    blob = blob.uploadFrom(input);
+    assertSame(expectedBlob, blob);
+  }
+
+  @Test
+  public void testUploadFromStreamRetrieveFailed() throws Exception {
+    byte[] dataToSend = {1, 2, 3, 4, 5};
+    StorageException storageException = new StorageException(123, "message");
+    WriteChannel channel = createWriteChannelMock(dataToSend);
+    initializeExpectedBlob(1);
+    BlobId blobId = BlobId.of(BLOB_INFO.getBucket(), BLOB_INFO.getName());
+    expect(storage.getOptions()).andReturn(mockOptions);
+    expect(storage.writer(eq(expectedBlob))).andReturn(channel);
+    expect(storage.get(blobId)).andThrow(storageException);
+    replay(storage);
+    Blob blob = new Blob(storage, new BlobInfo.BuilderImpl(BLOB_INFO));
+    InputStream input = new ByteArrayInputStream(dataToSend);
+    try {
+      blob.uploadFrom(input);
+      fail();
+    } catch (StorageException e) {
+      assertEquals(
+          "Content has been uploaded successfully. Failed to retrieve blob.", e.getMessage());
+      assertSame(e.getCause(), storageException);
+    }
+  }
+
+  @Test
+  public void testUpload() throws Exception {
+    replay(storage);
+    byte[] dataToSend = {1, 2, 3, 4, 5};
+    WriteChannel channel = createWriteChannelMock(dataToSend);
+    InputStream input = new ByteArrayInputStream(dataToSend);
+    Blob.uploadFrom(input, channel);
+  }
+
+  @Test
+  public void testUploadSmallBufferSize() throws Exception {
+    replay(storage);
+    byte[] dataToSend = new byte[100_000];
+    WriteChannel channel = createWriteChannelMock(dataToSend);
+    InputStream input = new ByteArrayInputStream(dataToSend);
+    Blob.uploadFrom(input, channel, 100);
+  }
+
+  @Test
+  public void testUploadMultiplePortions() throws Exception {
+    replay(storage);
+    int totalSize = 400_000;
+    int bufferSize = 300_000;
+    byte[] dataToSend = new byte[totalSize];
+    dataToSend[0] = 42;
+    dataToSend[bufferSize] = 43;
+
+    WriteChannel channel = createMock(WriteChannel.class);
+    expect(channel.write(ByteBuffer.wrap(dataToSend, 0, bufferSize))).andReturn(bufferSize);
+    expect(channel.write(ByteBuffer.wrap(dataToSend, bufferSize, totalSize - bufferSize)))
+        .andReturn(bufferSize - bufferSize);
+    channel.close();
+    replay(channel);
+    InputStream input = new ByteArrayInputStream(dataToSend);
+    Blob.uploadFrom(input, channel, bufferSize);
+  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageExceptionTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageExceptionTest.java
index 5060fdc6..e065f401 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageExceptionTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageExceptionTest.java
@@ -32,7 +32,9 @@ import com.google.api.client.http.HttpResponseException;
 import com.google.cloud.BaseServiceException;
 import com.google.cloud.RetryHelper.RetryHelperException;
 import java.io.IOException;
+import java.net.SocketException;
 import java.net.SocketTimeoutException;
+import javax.net.ssl.SSLException;
 import org.junit.Test;
 
 public class StorageExceptionTest {
@@ -135,6 +137,29 @@ public class StorageExceptionTest {
     assertTrue(exception.isRetryable());
   }
 
+  @Test
+  public void testTranslateConnectionReset() {
+    StorageException exception =
+        StorageException.translate(
+            new IOException(
+                "Connection has been shutdown: "
+                    + new SSLException(new SocketException("Connection reset"))));
+    assertEquals(0, exception.getCode());
+    assertEquals("connectionReset", exception.getReason());
+    assertTrue(exception.isRetryable());
+  }
+
+  @Test
+  public void testTranslateConnectionClosedPrematurely() {
+    StorageException exception =
+        StorageException.translate(
+            new IOException(
+                "Connection closed prematurely: bytesRead = 1114112, Content-Length = 10485760"));
+    assertEquals(0, exception.getCode());
+    assertEquals("connectionClosedPrematurely", exception.getReason());
+    assertTrue(exception.isRetryable());
+  }
+
   @Test
   public void testTranslateAndThrow() throws Exception {
     Exception cause = new StorageException(503, "message");
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 f9b45c87..31dc630f 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
@@ -40,6 +40,7 @@ import com.google.cloud.Identity;
 import com.google.cloud.Policy;
 import com.google.cloud.ReadChannel;
 import com.google.cloud.RestorableState;
+import com.google.cloud.RetryHelper;
 import com.google.cloud.TransportOptions;
 import com.google.cloud.WriteChannel;
 import com.google.cloud.http.HttpTransportOptions;
@@ -103,6 +104,8 @@ import java.io.InputStream;
 import java.net.URL;
 import java.net.URLConnection;
 import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.security.Key;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -3160,4 +3163,56 @@ public class ITStorageTest {
       RemoteStorageHelper.forceDelete(storage, loggingBucket, 5, TimeUnit.SECONDS);
     }
   }
+
+  @Test
+  public void testUpload() throws Exception {
+    String blobName = "test-upload-static";
+    BlobId blobId = BlobId.of(BUCKET, blobName);
+    try (WriteChannel writer = storage.writer(BlobInfo.newBuilder(blobId).build())) {
+      Blob.uploadFrom(new ByteArrayInputStream(BLOB_STRING_CONTENT.getBytes(UTF_8)), writer, 1);
+    }
+    Blob blob = storage.get(blobId);
+    String readString = new String(blob.getContent(), UTF_8);
+    assertEquals(BLOB_STRING_CONTENT, readString);
+  }
+
+  @Test
+  public void testUploadFromDownloadTo() throws Exception {
+    String blobName = "test-uploadFrom-downloadTo-blob";
+    BlobInfo blobInfo = BlobInfo.newBuilder(BUCKET, blobName).build();
+
+    Path tempFileFrom = Files.createTempFile("ITStorageTest_", ".tmp");
+    Files.write(tempFileFrom, BLOB_BYTE_CONTENT);
+    Blob blob = storage.create(blobInfo);
+    blob = blob.uploadFrom(tempFileFrom);
+
+    Path tempFileTo = Files.createTempFile("ITStorageTest_", ".tmp");
+    blob.downloadTo(tempFileTo);
+    byte[] readBytes = Files.readAllBytes(tempFileTo);
+    assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
+  }
+
+  @Test
+  public void testUploadFromDownloadToWithEncryption() throws Exception {
+    String blobName = "test-uploadFrom-downloadTo-withEncryption-blob";
+    BlobInfo blobInfo = BlobInfo.newBuilder(BUCKET, blobName).build();
+
+    Path tempFileFrom = Files.createTempFile("ITStorageTest_", ".tmp");
+    Files.write(tempFileFrom, BLOB_BYTE_CONTENT);
+    Blob blob = storage.create(blobInfo);
+    blob = blob.uploadFrom(tempFileFrom, Storage.BlobWriteOption.encryptionKey(KEY));
+
+    Path tempFileTo = Files.createTempFile("ITStorageTest_", ".tmp");
+    try {
+      blob.downloadTo(tempFileTo);
+    } catch (RetryHelper.RetryHelperException e) {
+      // Expected to be StorageException
+      String expectedMessage =
+          "The target object is encrypted by a customer-supplied encryption key.";
+      assertTrue(e.getMessage().contains(expectedMessage));
+    }
+    blob.downloadTo(tempFileTo, Blob.BlobSourceOption.decryptionKey(KEY));
+    byte[] readBytes = Files.readAllBytes(tempFileTo);
+    assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
+  }
 }