Repository: googleapis/java-storage (java)

Issue:
Provide an efficient way to download the latest version of an object
`Blob#downloadTo` is the library's efficient method for downloading blob data to an arbitrary sink. It is efficient because, unlike `Storage#reader`, it downloads the blob in one request as opposed to many, and because it performs much less copying. This results in significantly higher throughput and significantly lower memory pressure.

The sequence of `Storage#get` followed by `Blob#downloadTo`, which is required to use this API, is prone to races. This is due to the way that the blob's generation, as it existed at `Storage#get`, is unconditionally encoded into the HTTP request generated by `Blob#downloadTo`. If the bucket does not use versioning, the generation may change between `Storage#get` and `Blob#downloadTo`, leading to a 404.

With `Storage#reader` we can avoid this problem by not specifying the generation, for example using `reader(String, String)`, which will read the latest generation. No such workaround is possible with `Blob#downloadTo`, meaning that a usage of `Blob#downloadTo` has to have a fallback that catches the 404 and retries using `Storage#reader` with the generation unspecified. This is inefficient and inelegant, and should not be required.


Pull request description:
feat: Add downloadTo to the Storage interface
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:

Fixes #1158  ☕️

If you write sample code, please follow the [samples format](
https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e929578..15d0a2cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
 # Changelog
 
+### [2.6.1](https://github.com/googleapis/java-storage/compare/v2.6.0...v2.6.1) (2022-04-15)
+
+
+### Bug Fixes
+
+* add gccl-invocation-id interceptor ([#1309](https://github.com/googleapis/java-storage/issues/1309)) ([335c267](https://github.com/googleapis/java-storage/commit/335c2679b70f0bcd4db895d9cb4cbe97175e8070))
+* **java:** add service account email to Native Image testing kokoro job ([#1348](https://github.com/googleapis/java-storage/issues/1348)) ([9f76fcc](https://github.com/googleapis/java-storage/commit/9f76fccfddcc0d3a671ec4281dab303da07b9959))
+
+
+### Documentation
+
+* Adding PubSub Notification Samples ([#1317](https://github.com/googleapis/java-storage/issues/1317)) ([fa9920d](https://github.com/googleapis/java-storage/commit/fa9920d9097cfe6863c3e733a091c9b867d603ef))
+* Adding Samples for Creating Dual Region Buckets ([#1341](https://github.com/googleapis/java-storage/issues/1341)) ([9396061](https://github.com/googleapis/java-storage/commit/9396061ac71f98efd6784c34da3bbea04f48873d))
+* Update CreateBucketPubSubNotification to have line without horizontal scrolling ([#1335](https://github.com/googleapis/java-storage/issues/1335)) ([09b7842](https://github.com/googleapis/java-storage/commit/09b78424f7090c7c0469709a357a06143668e31b))
+
+
+### Dependencies
+
+* update dependency com.google.apis:google-api-services-storage to v1-rev20220401-1.32.1 ([#1337](https://github.com/googleapis/java-storage/issues/1337)) ([a5050e2](https://github.com/googleapis/java-storage/commit/a5050e230a620ba727a93c0a93f0bf82a011ce99))
+* update dependency com.google.cloud:google-cloud-pubsub to v1.116.3 ([#1327](https://github.com/googleapis/java-storage/issues/1327)) ([9d8c520](https://github.com/googleapis/java-storage/commit/9d8c520acca7f56f5af46348bc1db71bda1f93aa))
+* update dependency com.google.cloud:google-cloud-shared-dependencies to v2.10.0 ([#1352](https://github.com/googleapis/java-storage/issues/1352)) ([ab46f98](https://github.com/googleapis/java-storage/commit/ab46f985768c1539babf4c14a7e030083776ce0e))
+* update dependency com.google.cloud:native-image-support to v0.12.11 ([#1319](https://github.com/googleapis/java-storage/issues/1319)) ([c338c54](https://github.com/googleapis/java-storage/commit/c338c54210940dbe3b97aa0e7b13904e72ede91d))
+* update dependency com.google.cloud:native-image-support to v0.13.1 ([#1353](https://github.com/googleapis/java-storage/issues/1353)) ([0f76d27](https://github.com/googleapis/java-storage/commit/0f76d2773bc159b1c8a9eeddb54ae8406da86e7a))
+* update kms.version to v0.95.2 ([#1331](https://github.com/googleapis/java-storage/issues/1331)) ([2ca4883](https://github.com/googleapis/java-storage/commit/2ca488362ce2cb2b620ed6dc846d76b095d44a31))
+* update kms.version to v0.95.3 ([#1346](https://github.com/googleapis/java-storage/issues/1346)) ([a4f9503](https://github.com/googleapis/java-storage/commit/a4f95038e56ac36badf68edd072705621fec1dbe))
+
 ## [2.6.0](https://github.com/googleapis/java-storage/compare/v2.5.1...v2.6.0) (2022-03-30)
 
 
diff --git a/README.md b/README.md
index f6208c76..e04f27cf 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,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>2.6.0</version>
+  <version>2.6.1</version>
 </dependency>
 
 ```
@@ -56,13 +56,13 @@ implementation 'com.google.cloud:google-cloud-storage'
 If you are using Gradle without BOM, add this to your dependencies
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-storage:2.6.0'
+implementation 'com.google.cloud:google-cloud-storage:2.6.1'
 ```
 
 If you are using SBT, add this to your dependencies
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.6.0"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.6.1"
 ```
 
 ## Authentication
@@ -301,6 +301,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-storage/tree/
 | Copy Old Version Of Object | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/CopyOldVersionOfObject.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/CopyOldVersionOfObject.java) |
 | Delete Object | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/DeleteObject.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/DeleteObject.java) |
 | Delete Old Version Of Object | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/DeleteOldVersionOfObject.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/DeleteOldVersionOfObject.java) |
+| Download Byte Range | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/DownloadByteRange.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/DownloadByteRange.java) |
 | Download Encrypted Object | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/DownloadEncryptedObject.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/DownloadEncryptedObject.java) |
 | Download Object | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/DownloadObject.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/DownloadObject.java) |
 | Download Object Into Memory | [source code](https://github.com/googleapis/java-storage/blob/main/samples/snippets/src/main/java/com/example/storage/object/DownloadObjectIntoMemory.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/object/DownloadObjectIntoMemory.java) |
diff --git a/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml
index 96429fbd..da1dd149 100644
--- a/gapic-google-cloud-storage-v2/pom.xml
+++ b/gapic-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>gapic-google-cloud-storage-v2</artifactId>
-  <version>2.6.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+  <version>2.6.2-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
   <name>gapic-google-cloud-storage-v2</name>
   <description>GRPC library for gapic-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.6.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.6.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index 84fba3dc..8aabcf10 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -1,4 +1,14 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- see https://www.mojohaus.org/clirr-maven-plugin/examples/ignored-differences.html -->
 <differences>
+  <difference>
+    <className>com/google/cloud/storage/Storage*</className>
+    <differenceType>7012</differenceType>
+    <method>* downloadTo(com.google.cloud.storage.BlobId, java.io.OutputStream, com.google.cloud.storage.Storage$BlobSourceOption[])</method>
+  </difference>
+  <difference>
+    <className>com/google/cloud/storage/Storage*</className>
+    <differenceType>7012</differenceType>
+    <method>* downloadTo(com.google.cloud.storage.BlobId, java.nio.file.Path, com.google.cloud.storage.Storage$BlobSourceOption[])</method>
+  </difference>
 </differences>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 6ca2537e..fe8086ed 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>2.6.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.6.2-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>2.6.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.6.2-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 a1989699..79167aec 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
@@ -19,9 +19,7 @@ package com.google.cloud.storage;
 import static com.google.cloud.storage.Blob.BlobSourceOption.toGetOptions;
 import static com.google.cloud.storage.Blob.BlobSourceOption.toSourceOptions;
 import static com.google.common.base.Preconditions.checkNotNull;
-import static java.util.concurrent.Executors.callable;
 
-import com.google.api.gax.retrying.ResultRetryAlgorithm;
 import com.google.api.services.storage.model.StorageObject;
 import com.google.auth.ServiceAccountSigner;
 import com.google.auth.ServiceAccountSigner.SigningException;
@@ -34,12 +32,10 @@ import com.google.cloud.storage.Storage.CopyRequest;
 import com.google.cloud.storage.Storage.SignUrlOption;
 import com.google.cloud.storage.spi.v1.StorageRpc;
 import com.google.common.io.BaseEncoding;
-import com.google.common.io.CountingOutputStream;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.OutputStream;
 import java.net.URL;
-import java.nio.file.Files;
 import java.nio.file.Path;
 import java.security.Key;
 import java.util.Arrays;
@@ -47,7 +43,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
 
 /**
  * An object in Google Cloud Storage. A {@code Blob} object includes the {@code BlobId} instance,
@@ -231,11 +226,7 @@ public class Blob extends BlobInfo {
    * @throws StorageException upon failure
    */
   public void downloadTo(Path path, BlobSourceOption... options) {
-    try (OutputStream outputStream = Files.newOutputStream(path)) {
-      downloadTo(outputStream, options);
-    } catch (IOException e) {
-      throw new StorageException(e);
-    }
+    storage.downloadTo(getBlobId(), path, BlobSourceOption.toSourceOptions(this, options));
   }
 
   /**
@@ -245,20 +236,7 @@ public class Blob extends BlobInfo {
    * @param options
    */
   public void downloadTo(OutputStream outputStream, BlobSourceOption... options) {
-    final CountingOutputStream countingOutputStream = new CountingOutputStream(outputStream);
-    final StorageRpc storageRpc = this.options.getStorageRpcV1();
-    StorageObject pb = getBlobId().toPb();
-    final Map<StorageRpc.Option, ?> requestOptions = StorageImpl.optionMap(getBlobId(), options);
-    ResultRetryAlgorithm<?> algorithm = retryAlgorithmManager.getForObjectsGet(pb, requestOptions);
-    Retrying.run(
-        this.options,
-        algorithm,
-        callable(
-            () -> {
-              storageRpc.read(
-                  pb, requestOptions, countingOutputStream.getCount(), countingOutputStream);
-            }),
-        Function.identity());
+    storage.downloadTo(getBlobId(), outputStream, BlobSourceOption.toSourceOptions(this, options));
   }
 
   /**
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 634155f2..d27de901 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
@@ -31,6 +31,7 @@ import com.google.cloud.Service;
 import com.google.cloud.Tuple;
 import com.google.cloud.WriteChannel;
 import com.google.cloud.storage.Acl.Entity;
+import com.google.cloud.storage.Blob.BlobSourceOption;
 import com.google.cloud.storage.HmacKey.HmacKeyMetadata;
 import com.google.cloud.storage.PostPolicyV4.PostConditionsV4;
 import com.google.cloud.storage.PostPolicyV4.PostFieldsV4;
@@ -41,6 +42,7 @@ import com.google.common.collect.Lists;
 import com.google.common.io.BaseEncoding;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.Serializable;
 import java.net.URL;
 import java.nio.file.Path;
@@ -2672,6 +2674,45 @@ public interface Storage extends Service<StorageOptions> {
    */
   ReadChannel reader(BlobId blob, BlobSourceOption... options);
 
+  /**
+   * Downloads the given blob to the given path using specified blob read options.
+   *
+   * <pre>{@code
+   * String bucketName = "my-unique-bucket";
+   * String blobName = "my-blob-name";
+   * BlobId blobId = BlobId.of(bucketName, blobName);
+   * Path destination = Paths.get("my-blob-destination.txt");
+   * downloadTo(blobId, destination);
+   * // do stuff with destination
+   * }</pre>
+   *
+   * @param blob
+   * @param path
+   * @param options
+   * @throws StorageException upon failure
+   */
+  void downloadTo(BlobId blob, Path path, BlobSourceOption... options);
+
+  /**
+   * Downloads the given blob to the given output stream using specified blob read options.
+   *
+   * <pre>{@code
+   * String bucketName = "my-unique-bucket";
+   * String blobName = "my-blob-name";
+   * BlobId blobId = BlobId.of(bucketName, blobName);
+   * Path destination = Paths.get("my-blob-destination.txt");
+   * try (OutputStream outputStream = Files.newOutputStream(path)) {
+   *  downloadTo(blob, outputStream);
+   *  // do stuff with destination
+   * }
+   * }</pre>
+   *
+   * @param blob
+   * @param outputStream
+   * @param options
+   */
+  void downloadTo(BlobId blob, OutputStream outputStream, BlobSourceOption... options);
+
   /**
    * 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
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
index 874d997e..bf372c88 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
@@ -32,6 +32,7 @@ import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.Executors.callable;
 
 import com.google.api.gax.paging.Page;
 import com.google.api.gax.retrying.ResultRetryAlgorithm;
@@ -49,6 +50,7 @@ import com.google.cloud.RetryHelper.RetryHelperException;
 import com.google.cloud.Tuple;
 import com.google.cloud.WriteChannel;
 import com.google.cloud.storage.Acl.Entity;
+import com.google.cloud.storage.Blob.BlobSourceOption;
 import com.google.cloud.storage.HmacKey.HmacKeyMetadata;
 import com.google.cloud.storage.PostPolicyV4.ConditionV4Type;
 import com.google.cloud.storage.PostPolicyV4.PostConditionsV4;
@@ -67,10 +69,12 @@ import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.hash.Hashing;
 import com.google.common.io.BaseEncoding;
+import com.google.common.io.CountingOutputStream;
 import com.google.common.primitives.Ints;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
 import java.net.URI;
@@ -543,6 +547,32 @@ final class StorageImpl extends BaseService<StorageOptions> implements Storage {
     return new BlobReadChannel(getOptions(), blob, optionsMap);
   }
 
+  @Override
+  public void downloadTo(BlobId blob, Path path, BlobSourceOption... options) {
+    try (OutputStream outputStream = Files.newOutputStream(path)) {
+      downloadTo(blob, outputStream, options);
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
+  }
+
+  @Override
+  public void downloadTo(BlobId blob, OutputStream outputStream, BlobSourceOption... options) {
+    final CountingOutputStream countingOutputStream = new CountingOutputStream(outputStream);
+    final StorageObject pb = blob.toPb();
+    final Map<StorageRpc.Option, ?> requestOptions = optionMap(blob, options);
+    ResultRetryAlgorithm<?> algorithm = retryAlgorithmManager.getForObjectsGet(pb, requestOptions);
+    Retrying.run(
+        getOptions(),
+        algorithm,
+        callable(
+            () -> {
+              storageRpc.read(
+                  pb, requestOptions, countingOutputStream.getCount(), countingOutputStream);
+            }),
+        Function.identity());
+  }
+
   @Override
   public BlobWriteChannel writer(BlobInfo blobInfo, BlobWriteOption... options) {
     Tuple<BlobInfo, BlobTargetOption[]> targetOptions = BlobTargetOption.convert(blobInfo, options);
diff --git a/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml
index 9a32c1b3..976bfb9d 100644
--- a/grpc-google-cloud-storage-v2/pom.xml
+++ b/grpc-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-storage-v2</artifactId>
-  <version>2.6.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+  <version>2.6.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
   <name>grpc-google-cloud-storage-v2</name>
   <description>GRPC library for grpc-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.6.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.6.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/pom.xml b/pom.xml
index aff97f72..545cd1a1 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>2.6.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.6.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -70,7 +70,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.6.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.6.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.apis</groupId>
@@ -111,17 +111,17 @@
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.6.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.6.2-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.6.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.6.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.6.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.6.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml
index a594cd99..1b65712e 100644
--- a/proto-google-cloud-storage-v2/pom.xml
+++ b/proto-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-storage-v2</artifactId>
-  <version>2.6.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+  <version>2.6.2-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
   <name>proto-google-cloud-storage-v2</name>
   <description>PROTO library for proto-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.6.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.6.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index add91938..8f57741e 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -30,7 +30,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.6.0</version>
+      <version>2.6.1</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index f8a74963..1d33de0f 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>2.6.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.6.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
     </dependency>
 
     <dependency>
diff --git a/samples/snippets/src/main/java/com/example/storage/object/DownloadByteRange.java b/samples/snippets/src/main/java/com/example/storage/object/DownloadByteRange.java
new file mode 100644
index 00000000..b7327a72
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/storage/object/DownloadByteRange.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 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.example.storage.object;
+
+// [START storage_download_byte_range]
+
+import com.google.cloud.ReadChannel;
+import com.google.cloud.storage.BlobId;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+
+public class DownloadByteRange {
+
+  public static void downloadByteRange(
+      String projectId,
+      String bucketName,
+      String blobName,
+      long startByte,
+      long endBytes,
+      String destFileName)
+      throws IOException {
+    // The ID of your GCP project
+    // String projectId = "your-project-id";
+
+    // The ID of your GCS bucket
+    // String bucketName = "your-unique-bucket-name";
+
+    // The name of the blob/file that you wish to modify permissions on
+    // String blobName = "your-blob-name";
+
+    // The starting byte at which to begin the download
+    // long startByte = 0;
+
+    // The ending byte at which to end the download
+    // long endByte = 20;
+
+    // The path to which the file should be downloaded
+    // String destFileName = '/local/path/to/file.txt';
+
+    Storage storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService();
+    BlobId blobId = BlobId.of(bucketName, blobName);
+    try (ReadChannel from = storage.reader(blobId);
+        FileChannel to = FileChannel.open(Paths.get(destFileName), StandardOpenOption.WRITE)) {
+      from.seek(startByte);
+      from.limit(endBytes);
+
+      ByteStreams.copy(from, to);
+
+      System.out.printf(
+          "%s downloaded to %s from byte %d to byte %d",
+          blobId.toGsUtilUri(), destFileName, startByte, endBytes);
+    }
+  }
+}
+// [END storage_download_byte_range]
diff --git a/versions.txt b/versions.txt
index dc4ac15e..7df2a103 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,7 +1,7 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:2.6.0:2.6.1-SNAPSHOT
-gapic-google-cloud-storage-v2:2.6.0-alpha:2.6.1-alpha-SNAPSHOT
-grpc-google-cloud-storage-v2:2.6.0-alpha:2.6.1-alpha-SNAPSHOT
-proto-google-cloud-storage-v2:2.6.0-alpha:2.6.1-alpha-SNAPSHOT
+google-cloud-storage:2.6.1:2.6.2-SNAPSHOT
+gapic-google-cloud-storage-v2:2.6.1-alpha:2.6.2-alpha-SNAPSHOT
+grpc-google-cloud-storage-v2:2.6.1-alpha:2.6.2-alpha-SNAPSHOT
+proto-google-cloud-storage-v2:2.6.1-alpha:2.6.2-alpha-SNAPSHOT


Test patch:
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 55534975..97a9b7e2 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
@@ -16,14 +16,11 @@
 
 package com.google.cloud.storage;
 
-import static org.easymock.EasyMock.anyObject;
 import static org.easymock.EasyMock.capture;
 import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.createNiceMock;
 import static org.easymock.EasyMock.createStrictMock;
 import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.getCurrentArguments;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
 import static org.junit.Assert.assertArrayEquals;
@@ -32,11 +29,9 @@ 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.storage.Acl.Project;
 import com.google.cloud.storage.Acl.Project.ProjectRole;
@@ -45,21 +40,16 @@ import com.google.cloud.storage.Acl.User;
 import com.google.cloud.storage.Blob.BlobSourceOption;
 import com.google.cloud.storage.Storage.BlobWriteOption;
 import com.google.cloud.storage.Storage.CopyRequest;
-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.File;
-import java.io.OutputStream;
 import java.net.URL;
-import java.nio.file.Files;
 import java.security.Key;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import javax.crypto.spec.SecretKeySpec;
 import org.easymock.Capture;
-import org.easymock.IAnswer;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -612,102 +602,4 @@ public class BlobTest {
     assertNull(blob.getCustomTime());
     assertTrue(blob.isDirectory());
   }
-
-  private StorageRpc prepareForDownload() {
-    StorageRpc mockStorageRpc = createNiceMock(StorageRpc.class);
-    expect(storage.getOptions()).andReturn(mockOptions).anyTimes();
-    replay(storage);
-    expect(mockOptions.getStorageRpcV1()).andReturn(mockStorageRpc);
-    expect(mockOptions.getRetrySettings()).andReturn(RETRY_SETTINGS);
-    expect(mockOptions.getClock()).andReturn(API_CLOCK);
-    expect(mockOptions.getRetryAlgorithmManager()).andReturn(retryAlgorithmManager).anyTimes();
-    replay(mockOptions);
-    blob = new Blob(storage, new BlobInfo.BuilderImpl(BLOB_INFO));
-    return mockStorageRpc;
-  }
-
-  @Test
-  public void testDownloadTo() throws Exception {
-    final byte[] expected = {1, 2};
-    StorageRpc mockStorageRpc = prepareForDownload();
-    expect(
-            mockStorageRpc.read(
-                anyObject(StorageObject.class),
-                anyObject(Map.class),
-                eq(0l),
-                anyObject(OutputStream.class)))
-        .andAnswer(
-            new IAnswer<Long>() {
-              @Override
-              public Long answer() throws Throwable {
-                ((OutputStream) getCurrentArguments()[3]).write(expected);
-                return 2l;
-              }
-            });
-    replay(mockStorageRpc);
-    File file = File.createTempFile("blob", ".tmp");
-    blob.downloadTo(file.toPath());
-    byte actual[] = Files.readAllBytes(file.toPath());
-    assertArrayEquals(expected, actual);
-  }
-
-  @Test
-  public void testDownloadToWithRetries() throws Exception {
-    final byte[] expected = {1, 2};
-    StorageRpc mockStorageRpc = prepareForDownload();
-    expect(
-            mockStorageRpc.read(
-                anyObject(StorageObject.class),
-                anyObject(Map.class),
-                eq(0l),
-                anyObject(OutputStream.class)))
-        .andAnswer(
-            new IAnswer<Long>() {
-              @Override
-              public Long answer() throws Throwable {
-                ((OutputStream) getCurrentArguments()[3]).write(expected[0]);
-                throw new StorageException(504, "error");
-              }
-            });
-    expect(
-            mockStorageRpc.read(
-                anyObject(StorageObject.class),
-                anyObject(Map.class),
-                eq(1l),
-                anyObject(OutputStream.class)))
-        .andAnswer(
-            new IAnswer<Long>() {
-              @Override
-              public Long answer() throws Throwable {
-                ((OutputStream) getCurrentArguments()[3]).write(expected[1]);
-                return 1l;
-              }
-            });
-    replay(mockStorageRpc);
-    File file = File.createTempFile("blob", ".tmp");
-    blob.downloadTo(file.toPath());
-    byte actual[] = Files.readAllBytes(file.toPath());
-    assertArrayEquals(expected, actual);
-  }
-
-  @Test
-  public void testDownloadToWithException() throws Exception {
-    StorageRpc mockStorageRpc = prepareForDownload();
-    Exception exception = new IllegalStateException("test");
-    expect(
-            mockStorageRpc.read(
-                anyObject(StorageObject.class),
-                anyObject(Map.class),
-                eq(0l),
-                anyObject(OutputStream.class)))
-        .andThrow(exception);
-    replay(mockStorageRpc);
-    File file = File.createTempFile("blob", ".tmp");
-    try {
-      blob.downloadTo(file.toPath());
-      fail();
-    } catch (StorageException e) {
-      assertSame(exception, e.getCause());
-    }
-  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java
index 5619e5f7..ae368241 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java
@@ -19,6 +19,11 @@ package com.google.cloud.storage;
 import static com.google.cloud.storage.SignedUrlEncodingHelper.Rfc3986UriEncode;
 import static com.google.cloud.storage.testing.ApiPolicyMatcher.eqApiPolicy;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.easymock.EasyMock.replay;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -27,10 +32,12 @@ import static org.junit.Assert.assertNotNull;
 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.client.googleapis.json.GoogleJsonError;
 import com.google.api.client.util.DateTime;
 import com.google.api.core.ApiClock;
+import com.google.api.gax.retrying.RetrySettings;
 import com.google.api.services.storage.model.Policy.Bindings;
 import com.google.api.services.storage.model.StorageObject;
 import com.google.api.services.storage.model.TestIamPermissionsResponse;
@@ -53,9 +60,12 @@ 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.File;
+import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.URL;
 import java.net.URLDecoder;
+import java.nio.file.Files;
 import java.security.InvalidKeyException;
 import java.security.Key;
 import java.security.KeyFactory;
@@ -77,6 +87,7 @@ import java.util.regex.Pattern;
 import javax.crypto.spec.SecretKeySpec;
 import org.easymock.Capture;
 import org.easymock.EasyMock;
+import org.easymock.IAnswer;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -2261,4 +2272,97 @@ public class StorageImplTest {
     assertEquals(30, lifecycleRule.getCondition().getDaysSinceCustomTime().intValue());
     assertNotNull(lifecycleRule.getCondition().getCustomTimeBefore());
   }
+
+  @Test
+  public void testDownloadTo() throws Exception {
+    BlobId blob = BlobId.of(BUCKET_NAME1, BLOB_NAME1);
+    storage = options.toBuilder().build().getService();
+    final byte[] expected = {1, 2};
+    EasyMock.expect(
+            storageRpcMock.read(
+                anyObject(StorageObject.class),
+                anyObject(Map.class),
+                eq(0l),
+                anyObject(OutputStream.class)))
+        .andAnswer(
+            new IAnswer<Long>() {
+              @Override
+              public Long answer() throws Throwable {
+                ((OutputStream) getCurrentArguments()[3]).write(expected);
+                return 2l;
+              }
+            });
+    EasyMock.replay(storageRpcMock);
+    File file = File.createTempFile("blob", ".tmp");
+    storage.downloadTo(blob, file.toPath());
+    byte actual[] = Files.readAllBytes(file.toPath());
+    assertArrayEquals(expected, actual);
+  }
+
+  @Test
+  public void testDownloadToWithRetries() throws Exception {
+    BlobId blob = BlobId.of(BUCKET_NAME1, BLOB_NAME1);
+    storage =
+        options
+            .toBuilder()
+            .setRetrySettings(RetrySettings.newBuilder().setMaxAttempts(2).build())
+            .build()
+            .getService();
+    final byte[] expected = {1, 2};
+    expect(
+            storageRpcMock.read(
+                anyObject(StorageObject.class),
+                anyObject(Map.class),
+                eq(0l),
+                anyObject(OutputStream.class)))
+        .andAnswer(
+            new IAnswer<Long>() {
+              @Override
+              public Long answer() throws Throwable {
+                ((OutputStream) getCurrentArguments()[3]).write(expected[0]);
+                throw new StorageException(504, "error");
+              }
+            });
+    expect(
+            storageRpcMock.read(
+                anyObject(StorageObject.class),
+                anyObject(Map.class),
+                eq(1l),
+                anyObject(OutputStream.class)))
+        .andAnswer(
+            new IAnswer<Long>() {
+              @Override
+              public Long answer() throws Throwable {
+                ((OutputStream) getCurrentArguments()[3]).write(expected[1]);
+                return 1l;
+              }
+            });
+    replay(storageRpcMock);
+    File file = File.createTempFile("blob", ".tmp");
+    storage.downloadTo(blob, file.toPath());
+    byte actual[] = Files.readAllBytes(file.toPath());
+    assertArrayEquals(expected, actual);
+  }
+
+  @Test
+  public void testDownloadToWithException() throws Exception {
+    BlobId blob = BlobId.of(BUCKET_NAME1, BLOB_NAME1);
+    storage = options.toBuilder().build().getService();
+    Exception exception = new IllegalStateException("test");
+    expect(
+            storageRpcMock.read(
+                anyObject(StorageObject.class),
+                anyObject(Map.class),
+                eq(0l),
+                anyObject(OutputStream.class)))
+        .andThrow(exception);
+    replay(storageRpcMock);
+    File file = File.createTempFile("blob", ".tmp");
+    try {
+      storage.downloadTo(blob, file.toPath());
+      fail();
+    } catch (StorageException e) {
+      assertSame(exception, e.getCause());
+    }
+  }
 }
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 d755d38d..d5aa1b6d 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
@@ -883,17 +883,16 @@ public class ITStorageTest {
     storage.createFrom(blobInfo, gzippedFile.toPath());
 
     Path rawInputGzippedFile = File.createTempFile("rawinputgzippedfile", ".txt").toPath();
-    Blob blob = storage.get(blobId);
 
-    blob.downloadTo(rawInputGzippedFile, Blob.BlobSourceOption.shouldReturnRawInputStream(true));
+    storage.downloadTo(
+        blobId, rawInputGzippedFile, Storage.BlobSourceOption.shouldReturnRawInputStream(true));
 
     assertArrayEquals(
         Files.readAllBytes(gzippedFile.toPath()), Files.readAllBytes(rawInputGzippedFile));
 
     Path unzippedFile = File.createTempFile("unzippedfile", ".txt").toPath();
-    storage
-        .get(blobId)
-        .downloadTo(unzippedFile, Blob.BlobSourceOption.shouldReturnRawInputStream(false));
+    storage.downloadTo(
+        blobId, unzippedFile, Storage.BlobSourceOption.shouldReturnRawInputStream(false));
 
     assertArrayEquals("hello world".getBytes(), Files.readAllBytes(unzippedFile));
   }
@@ -3686,7 +3685,7 @@ public class ITStorageTest {
     assertEquals(BLOB_BYTE_CONTENT.length, (long) blob.getSize());
 
     Path tempFileTo = Files.createTempFile("ITStorageTest_", ".tmp");
-    storage.get(blobId).downloadTo(tempFileTo);
+    storage.downloadTo(blobId, tempFileTo);
     byte[] readBytes = Files.readAllBytes(tempFileTo);
     assertArrayEquals(BLOB_BYTE_CONTENT, readBytes);
   }
diff --git a/samples/snippets/src/test/java/com/example/storage/object/DownloadBytesRangeTest.java b/samples/snippets/src/test/java/com/example/storage/object/DownloadBytesRangeTest.java
new file mode 100644
index 00000000..c20be679
--- /dev/null
+++ b/samples/snippets/src/test/java/com/example/storage/object/DownloadBytesRangeTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 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.example.storage.object;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.example.storage.TestBase;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class DownloadBytesRangeTest extends TestBase {
+
+  @Rule public final TemporaryFolder tmp = new TemporaryFolder();
+
+  @Test
+  public void testDownloadByteRange() throws IOException {
+    byte[] bytes = { // 18 elements per row
+      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
+      's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
+      's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
+      's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
+    };
+
+    blob = storage.create(blob, bytes);
+
+    File file = tmp.newFile();
+
+    int startByte = 14;
+    int endBytes = 37;
+    byte[] expectedBytes = Arrays.copyOfRange(bytes, startByte, endBytes);
+
+    try {
+      String destFileName = file.getAbsolutePath();
+      DownloadByteRange.downloadByteRange(
+          System.getenv("GOOGLE_CLOUD_PROJECT"),
+          bucketName,
+          blobName,
+          startByte,
+          endBytes,
+          destFileName);
+
+      byte[] readBytes = Files.readAllBytes(Paths.get(destFileName));
+
+      assertThat(stdOut.getCapturedOutputAsUtf8String()).contains("downloaded to");
+      assertThat(stdOut.getCapturedOutputAsUtf8String()).contains("from byte 14 to byte 37");
+      assertThat(readBytes).isEqualTo(expectedBytes);
+    } finally {
+      file.delete();
+    }
+  }
+}