Repository: googleapis/java-storage
Pull request #216 (2020-03-30 22:54:35)

chore: update common templates
This PR was generated using Autosynth. :rainbow:

Synth log will be available here:
https://source.cloud.google.com/results/invocations/8eb55052-e21f-4221-aaa3-aa6112c26a0e/targets

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

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

```diff
@@             Coverage Diff             @@
##             master    #216      +/-   ##
===========================================
+ Coverage     63.43%   63.5%   +0.06%     
  Complexity      540     540              
===========================================
  Files            30      30              
  Lines          4759    4759              
  Branches        427     427              
===========================================
+ Hits           3019    3022       +3     
+ Misses         1580    1577       -3     
  Partials        160     160
```


| [Impacted Files](https://codecov.io/gh/googleapis/java-storage/pull/216?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...gle/cloud/storage/testing/RemoteStorageHelper.java](https://codecov.io/gh/googleapis/java-storage/pull/216/diff?src=pr&el=tree#diff-Z29vZ2xlLWNsb3VkLXN0b3JhZ2Uvc3JjL21haW4vamF2YS9jb20vZ29vZ2xlL2Nsb3VkL3N0b3JhZ2UvdGVzdGluZy9SZW1vdGVTdG9yYWdlSGVscGVyLmphdmE=) | `63.93% <0%> (+2.45%)` | `9% <0%> (ø)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/googleapis/java-storage/pull/216?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/216?src=pr&el=footer). Last update [be74072...7c201b5](https://codecov.io/gh/googleapis/java-storage/pull/216?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.kokoro/build.sh b/.kokoro/build.sh
index f833a062..86bb6be3 100755
--- a/.kokoro/build.sh
+++ b/.kokoro/build.sh
@@ -20,17 +20,22 @@ scriptDir=$(realpath $(dirname "${BASH_SOURCE[0]}"))
 ## cd to the parent directory, i.e. the root of the git repo
 cd ${scriptDir}/..
 
+# include common functions
+source ${scriptDir}/common.sh
+
 # Print out Java version
 java -version
 echo ${JOB_TYPE}
 
-mvn install -B -V \
-  -DskipTests=true \
-  -Dclirr.skip=true \
-  -Denforcer.skip=true \
-  -Dmaven.javadoc.skip=true \
-  -Dgcloud.download.skip=true \
-  -T 1C
+# attempt to install 3 times with exponential backoff (starting with 10 seconds)
+retry_with_backoff 3 10 \
+  mvn install -B -V \
+    -DskipTests=true \
+    -Dclirr.skip=true \
+    -Denforcer.skip=true \
+    -Dmaven.javadoc.skip=true \
+    -Dgcloud.download.skip=true \
+    -T 1C
 
 # if GOOGLE_APPLICATION_CREDIENTIALS is specified as a relative path prepend Kokoro root directory onto it
 if [[ ! -z "${GOOGLE_APPLICATION_CREDENTIALS}" && "${GOOGLE_APPLICATION_CREDENTIALS}" != /* ]]; then
diff --git a/.kokoro/common.sh b/.kokoro/common.sh
new file mode 100644
index 00000000..a3bbc5f6
--- /dev/null
+++ b/.kokoro/common.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# set -eo pipefail
+
+function retry_with_backoff {
+    attempts_left=$1
+    sleep_seconds=$2
+    shift 2
+    command=$@
+
+    echo "${command}"
+    ${command}
+    exit_code=$?
+
+    if [[ $exit_code == 0 ]]
+    then
+        return 0
+    fi
+
+    # failure
+    if [[ ${attempts_left} > 0 ]]
+    then
+        echo "failure (${exit_code}), sleeping ${sleep_seconds}..."
+        sleep ${sleep_seconds}
+        new_attempts=$((${attempts_left} - 1))
+        new_sleep=$((${sleep_seconds} * 2))
+        retry_with_backoff ${new_attempts} ${new_sleep} ${command}
+    fi
+
+    return $exit_code
+}
diff --git a/.kokoro/dependencies.sh b/.kokoro/dependencies.sh
index 04a288b1..0aade871 100755
--- a/.kokoro/dependencies.sh
+++ b/.kokoro/dependencies.sh
@@ -15,7 +15,13 @@
 
 set -eo pipefail
 
-cd github/java-storage/
+## Get the directory of the build script
+scriptDir=$(realpath $(dirname "${BASH_SOURCE[0]}"))
+## cd to the parent directory, i.e. the root of the git repo
+cd ${scriptDir}/..
+
+# include common functions
+source ${scriptDir}/common.sh
 
 # Print out Java
 java -version
@@ -24,8 +30,9 @@ echo $JOB_TYPE
 export MAVEN_OPTS="-Xmx1024m -XX:MaxPermSize=128m"
 
 # this should run maven enforcer
-mvn install -B -V \
-  -DskipTests=true \
-  -Dclirr.skip=true
+retry_with_backoff 3 10 \
+  mvn install -B -V \
+    -DskipTests=true \
+    -Dclirr.skip=true
 
 mvn -B dependency:analyze -DfailOnWarning=true
diff --git a/.kokoro/linkage-monitor.sh b/.kokoro/linkage-monitor.sh
index 158641cb..759ab4e2 100755
--- a/.kokoro/linkage-monitor.sh
+++ b/.kokoro/linkage-monitor.sh
@@ -17,18 +17,26 @@ set -eo pipefail
 # Display commands being run.
 set -x
 
-cd github/java-storage/
+## Get the directory of the build script
+scriptDir=$(realpath $(dirname "${BASH_SOURCE[0]}"))
+## cd to the parent directory, i.e. the root of the git repo
+cd ${scriptDir}/..
+
+# include common functions
+source ${scriptDir}/common.sh
 
 # Print out Java version
 java -version
 echo ${JOB_TYPE}
 
-mvn install -B -V \
-  -DskipTests=true \
-  -Dclirr.skip=true \
-  -Denforcer.skip=true \
-  -Dmaven.javadoc.skip=true \
-  -Dgcloud.download.skip=true
+# attempt to install 3 times with exponential backoff (starting with 10 seconds)
+retry_with_backoff 3 10 \
+  mvn install -B -V \
+    -DskipTests=true \
+    -Dclirr.skip=true \
+    -Denforcer.skip=true \
+    -Dmaven.javadoc.skip=true \
+    -Dgcloud.download.skip=true
 
 # Kokoro job cloud-opensource-java/ubuntu/linkage-monitor-gcs creates this JAR
 JAR=linkage-monitor-latest-all-deps.jar
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOperations.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOperations.java
new file mode 100644
index 00000000..6ff178ff
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOperations.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import com.google.cloud.WriteChannel;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** Utility methods to perform various operations with the Storage such as upload. */
+public final class StorageOperations {
+
+  private final Storage storage;
+  private static final int DEFAULT_BUFFER_SIZE = 15 * 1024 * 1024;
+  private static final int MIN_BUFFER_SIZE = 256 * 1024;
+
+  /**
+   * Creates a new StorageOperations instance associated with the given storage.
+   *
+   * @param storage the Storage
+   */
+  public StorageOperations(Storage storage) {
+    this.storage = storage;
+  }
+
+  /**
+   * Uploads {@code path} to the blob using {@link Storage#writer}. By default any MD5 and CRC32C
+   * values in the given {@code blobInfo} are ignored unless requested via the {@link
+   * Storage.BlobWriteOption#md5Match()} and {@link Storage.BlobWriteOption#crc32cMatch()} options.
+   * Folder upload is not supported.
+   *
+   * <p>Example of uploading a file:
+   *
+   * <pre>{@code
+   * String bucketName = "my-unique-bucket";
+   * String fileName = "readme.txt";
+   * BlobId blobId = BlobId.of(bucketName, fileName);
+   * BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType("text/plain").build();
+   * new StorageOperations(storage).upload(blobInfo, Paths.get(fileName));
+   * }</pre>
+   *
+   * @param blobInfo blob to create
+   * @param path file to upload
+   * @param options blob write options
+   * @throws IOException on I/O error
+   * @throws StorageException on failure
+   * @see #upload(BlobInfo, Path, int, Storage.BlobWriteOption...)
+   */
+  public void upload(BlobInfo blobInfo, Path path, Storage.BlobWriteOption... options)
+      throws IOException {
+    upload(blobInfo, path, DEFAULT_BUFFER_SIZE, options);
+  }
+
+  /**
+   * Uploads {@code path} to the blob using {@link Storage#writer} and {@code bufferSize}. By
+   * default any MD5 and CRC32C values in the given {@code blobInfo} are ignored unless requested
+   * via the {@link Storage.BlobWriteOption#md5Match()} and {@link
+   * Storage.BlobWriteOption#crc32cMatch()} options. Folder upload is not supported.
+   *
+   * <p>{@link #upload(BlobInfo, Path, Storage.BlobWriteOption...)} invokes this method 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>Example of uploading a humongous file:
+   *
+   * <pre>{@code
+   * BlobId blobId = BlobId.of(bucketName, blobName);
+   * BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType("video/webm").build();
+   *
+   * int largeBufferSize = 150 * 1024 * 1024;
+   * Path file = Paths.get("humongous.file");
+   * new StorageOperations(storage).upload(blobInfo, file, largeBufferSize);
+   * }</pre>
+   *
+   * @param blobInfo blob to create
+   * @param path file to upload
+   * @param bufferSize size of the buffer I/O operations
+   * @param options blob write options
+   * @throws IOException on I/O error
+   * @throws StorageException on failure
+   */
+  public void upload(
+      BlobInfo blobInfo, Path path, int bufferSize, Storage.BlobWriteOption... options)
+      throws IOException {
+    if (Files.isDirectory(path)) {
+      throw new StorageException(0, path + " is a directory");
+    }
+    try (InputStream input = Files.newInputStream(path)) {
+      upload(blobInfo, input, bufferSize, options);
+    }
+  }
+
+  /**
+   * Reads bytes from an input stream and uploads those bytes to the blob using {@link
+   * Storage#writer}. By default any MD5 and CRC32C values in the given {@code blobInfo} are ignored
+   * unless requested via the {@link Storage.BlobWriteOption#md5Match()} and {@link
+   * Storage.BlobWriteOption#crc32cMatch()} options.
+   *
+   * <p>Example of uploading data with CRC32C checksum:
+   *
+   * <pre>{@code
+   * BlobId blobId = BlobId.of(bucketName, blobName);
+   * byte[] content = "Hello, world".getBytes(StandardCharsets.UTF_8);
+   * Hasher hasher = Hashing.crc32c().newHasher().putBytes(content);
+   * String crc32c = BaseEncoding.base64().encode(Ints.toByteArray(hasher.hash().asInt()));
+   * BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setCrc32c(crc32c).build();
+   * new StorageOperations(storage).upload(blobInfo, new ByteArrayInputStream(content),
+   *     Storage.BlobWriteOption.crc32cMatch());
+   * }</pre>
+   *
+   * @param blobInfo blob to create
+   * @param content input stream to read from
+   * @param options blob write options
+   * @throws IOException on I/O error
+   * @throws StorageException on failure
+   * @see #upload(BlobInfo, InputStream, int, Storage.BlobWriteOption...)
+   */
+  public void upload(BlobInfo blobInfo, InputStream content, Storage.BlobWriteOption... options)
+      throws IOException {
+    upload(blobInfo, content, DEFAULT_BUFFER_SIZE, options);
+  }
+
+  /**
+   * Reads bytes from an input stream and uploads those bytes to the blob using {@link
+   * Storage#writer} and {@code bufferSize}. By default any MD5 and CRC32C values in the given
+   * {@code blobInfo} are ignored unless requested via the {@link
+   * Storage.BlobWriteOption#md5Match()} and {@link Storage.BlobWriteOption#crc32cMatch()} options.
+   *
+   * <p>{@link #upload(BlobInfo, InputStream, Storage.BlobWriteOption...)} )} invokes this method
+   * 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.
+   *
+   * @param blobInfo blob to create
+   * @param content input stream to read from
+   * @param bufferSize size of the buffer I/O operations
+   * @param options blob write options
+   * @throws IOException on I/O error
+   * @throws StorageException on failure
+   */
+  public void upload(
+      BlobInfo blobInfo, InputStream content, int bufferSize, Storage.BlobWriteOption... options)
+      throws IOException {
+    try (WriteChannel writer = storage.writer(blobInfo, options)) {
+      upload(Channels.newChannel(content), writer, bufferSize);
+    }
+  }
+
+  /*
+   * Uploads the given content to the storage using specified write channel and the given buffer
+   * size. This method does not close any channels.
+   */
+  private static void upload(ReadableByteChannel reader, WriteChannel writer, int bufferSize)
+      throws IOException {
+    bufferSize = Math.max(bufferSize, MIN_BUFFER_SIZE);
+    ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
+    writer.setChunkSize(bufferSize);
+
+    while (reader.read(buffer) >= 0) {
+      buffer.flip();
+      writer.write(buffer);
+      buffer.clear();
+    }
+  }
+}
diff --git a/renovate.json b/renovate.json
index fc641270..1dcdf893 100644
--- a/renovate.json
+++ b/renovate.json
@@ -56,7 +56,9 @@
     },
     {
       "packagePatterns": [
-        "^com.google.cloud:libraries-bom"
+        "^com.google.cloud:google-cloud-storage",
+        "^com.google.cloud:libraries-bom",
+        "^com.google.cloud.samples:shared-configuration"
       ],
       "semanticCommitType": "chore",
       "semanticCommitScope": "deps"
@@ -75,4 +77,4 @@
     }
   ],
   "semanticCommits": true
-}
+}
\ No newline at end of file
diff --git a/synth.metadata b/synth.metadata
index 66cd387f..1a8f7a39 100644
--- a/synth.metadata
+++ b/synth.metadata
@@ -1,11 +1,11 @@
 {
-  "updateTime": "2020-03-17T18:53:43.239577Z",
+  "updateTime": "2020-03-30T22:54:24.805818Z",
   "sources": [
     {
-      "template": {
-        "name": "java_library",
-        "origin": "synthtool.gcp",
-        "version": "2020.2.4"
+      "git": {
+        "name": "synthtool",
+        "remote": "https://github.com/googleapis/synthtool.git",
+        "sha": "a003d8655d3ebec2bbbd5fc3898e91e152265c67"
       }
     }
   ]


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOperationsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOperationsTest.java
new file mode 100644
index 00000000..0651c1ac
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOperationsTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import com.google.cloud.WriteChannel;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+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 org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class StorageOperationsTest {
+  private Storage storage;
+  private StorageOperations storageOperations;
+
+  private static final BlobInfo BLOB_INFO = BlobInfo.newBuilder("b", "n").build();
+  private static final int DEFAULT_BUFFER_SIZE = 15 * 1024 * 1024;
+  private static final int MIN_BUFFER_SIZE = 256 * 1024;
+
+  @Before
+  public void setUp() {
+    storage = createStrictMock(Storage.class);
+    storageOperations = new StorageOperations(storage);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    verify(storage);
+  }
+
+  @Test
+  public void testUploadFromNonExistentFile() {
+    replay(storage);
+    String fileName = "non_existing_file.txt";
+    try {
+      storageOperations.upload(BLOB_INFO, Paths.get(fileName));
+      storageOperations.upload(BLOB_INFO, Paths.get(fileName), -1);
+      fail();
+    } catch (IOException e) {
+      assertEquals(NoSuchFileException.class, e.getClass());
+      assertEquals(fileName, e.getMessage());
+    }
+  }
+
+  @Test
+  public void testUploadFromDirectory() throws IOException {
+    replay(storage);
+    Path dir = Files.createTempDirectory("unit_");
+    try {
+      storageOperations.upload(BLOB_INFO, dir);
+      storageOperations.upload(BLOB_INFO, dir, -2);
+      fail();
+    } catch (StorageException e) {
+      assertEquals(dir + " is a directory", e.getMessage());
+    }
+  }
+
+  private void prepareForUpload(BlobInfo blobInfo, byte[] bytes, Storage.BlobWriteOption... options)
+      throws Exception {
+    prepareForUpload(blobInfo, bytes, DEFAULT_BUFFER_SIZE, options);
+  }
+
+  private void prepareForUpload(
+      BlobInfo blobInfo, byte[] bytes, int bufferSize, Storage.BlobWriteOption... options)
+      throws Exception {
+    WriteChannel channel = createStrictMock(WriteChannel.class);
+    ByteBuffer expectedByteBuffer = ByteBuffer.wrap(bytes, 0, bytes.length);
+    channel.setChunkSize(bufferSize);
+    expect(channel.write(expectedByteBuffer)).andReturn(bytes.length);
+    channel.close();
+    replay(channel);
+    expect(storage.writer(blobInfo, options)).andReturn(channel);
+    replay(storage);
+  }
+
+  @Test
+  public void testUploadFromFile() throws Exception {
+    byte[] dataToSend = {1, 2, 3};
+    prepareForUpload(BLOB_INFO, dataToSend);
+    Path tempFile = Files.createTempFile("testUpload", ".tmp");
+    Files.write(tempFile, dataToSend);
+    storageOperations.upload(BLOB_INFO, tempFile);
+  }
+
+  @Test
+  public void testUploadFromStream() throws Exception {
+    byte[] dataToSend = {1, 2, 3, 4, 5};
+    Storage.BlobWriteOption[] options =
+        new Storage.BlobWriteOption[] {Storage.BlobWriteOption.crc32cMatch()};
+    prepareForUpload(BLOB_INFO, dataToSend, options);
+    InputStream input = new ByteArrayInputStream(dataToSend);
+    storageOperations.upload(BLOB_INFO, input, options);
+  }
+
+  @Test
+  public void testUploadSmallBufferSize() throws Exception {
+    byte[] dataToSend = new byte[100_000];
+    prepareForUpload(BLOB_INFO, dataToSend, MIN_BUFFER_SIZE);
+    InputStream input = new ByteArrayInputStream(dataToSend);
+    int smallBufferSize = 100;
+    storageOperations.upload(BLOB_INFO, input, smallBufferSize);
+  }
+
+  @Test
+  public void testUploadFromIOException() throws Exception {
+    IOException ioException = new IOException("message");
+    WriteChannel channel = createStrictMock(WriteChannel.class);
+    channel.setChunkSize(DEFAULT_BUFFER_SIZE);
+    expect(channel.write((ByteBuffer) anyObject())).andThrow(ioException);
+    replay(channel);
+    expect(storage.writer(eq(BLOB_INFO))).andReturn(channel);
+    replay(storage);
+    InputStream input = new ByteArrayInputStream(new byte[10]);
+    try {
+      storageOperations.upload(BLOB_INFO, input);
+      fail();
+    } catch (IOException e) {
+      assertSame(e, ioException);
+    }
+  }
+
+  @Test
+  public void testUploadMultiplePortions() throws Exception {
+    int totalSize = 400_000;
+    int bufferSize = 300_000;
+    byte[] dataToSend = new byte[totalSize];
+    dataToSend[0] = 42;
+    dataToSend[bufferSize] = 43;
+
+    WriteChannel channel = createStrictMock(WriteChannel.class);
+    channel.setChunkSize(bufferSize);
+    expect(channel.write(ByteBuffer.wrap(dataToSend, 0, bufferSize))).andReturn(1);
+    expect(channel.write(ByteBuffer.wrap(dataToSend, bufferSize, totalSize - bufferSize)))
+        .andReturn(2);
+    channel.close();
+    replay(channel);
+    expect(storage.writer(BLOB_INFO)).andReturn(channel);
+    replay(storage);
+
+    InputStream input = new ByteArrayInputStream(dataToSend);
+    storageOperations.upload(BLOB_INFO, input, bufferSize);
+  }
+}
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 6abc233f..09411792 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
@@ -76,6 +76,7 @@ import com.google.cloud.storage.StorageBatch;
 import com.google.cloud.storage.StorageBatchResult;
 import com.google.cloud.storage.StorageClass;
 import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.StorageOperations;
 import com.google.cloud.storage.StorageOptions;
 import com.google.cloud.storage.StorageRoles;
 import com.google.cloud.storage.testing.RemoteStorageHelper;
@@ -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;
@@ -131,6 +134,7 @@ public class ITStorageTest {
 
   private static RemoteStorageHelper remoteStorageHelper;
   private static Storage storage;
+  private static StorageOperations storageOperations;
   private static String kmsKeyOneResourcePath;
   private static String kmsKeyTwoResourcePath;
   private static Metadata requestParamsHeader = new Metadata();
@@ -175,6 +179,7 @@ public class ITStorageTest {
   public static void beforeClass() throws IOException {
     remoteStorageHelper = RemoteStorageHelper.create();
     storage = remoteStorageHelper.getOptions().getService();
+    storageOperations = new StorageOperations(storage);
 
     storage.create(
         BucketInfo.newBuilder(BUCKET)
@@ -3208,4 +3213,43 @@ public class ITStorageTest {
       RemoteStorageHelper.forceDelete(storage, loggingBucket, 5, TimeUnit.SECONDS);
     }
   }
+
+  @Test
+  public void testUploadFromDownloadTo() throws Exception {
+    String blobName = "test-uploadFrom-downloadTo-blob";
+    BlobId blobId = BlobId.of(BUCKET, blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
+
+    Path tempFileFrom = Files.createTempFile("ITStorageTest_", ".tmp");
+    Files.write(tempFileFrom, BLOB_BYTE_CONTENT);
+    storageOperations.upload(blobInfo, tempFileFrom);
+
+    Path tempFileTo = Files.createTempFile("ITStorageTest_", ".tmp");
+    storage.get(blobId).downloadTo(tempFileTo);
+    byte[] readBytes = Files.readAllBytes(tempFileTo);
+    assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
+  }
+
+  @Test
+  public void testUploadWithEncryption() throws Exception {
+    String blobName = "test-upload-withEncryption";
+    BlobId blobId = BlobId.of(BUCKET, blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
+
+    ByteArrayInputStream content = new ByteArrayInputStream(BLOB_BYTE_CONTENT);
+    storageOperations.upload(blobInfo, content, Storage.BlobWriteOption.encryptionKey(KEY));
+
+    Blob blob = storage.get(blobId);
+    try {
+      blob.getContent();
+      fail("StorageException was expected");
+    } catch (StorageException e) {
+      String expectedMessage =
+          "The target object is encrypted by a customer-supplied encryption key.";
+      assertTrue(e.getMessage().contains(expectedMessage));
+      assertEquals(400, e.getCode());
+    }
+    byte[] readBytes = blob.getContent(Blob.BlobSourceOption.decryptionKey(KEY));
+    assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
+  }
 }