diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml index 84fba3dc3..8aabcf10b 100644 --- a/google-cloud-storage/clirr-ignored-differences.xml +++ b/google-cloud-storage/clirr-ignored-differences.xml @@ -1,4 +1,14 @@ + + com/google/cloud/storage/Storage* + 7012 + * downloadTo(com.google.cloud.storage.BlobId, java.io.OutputStream, com.google.cloud.storage.Storage$BlobSourceOption[]) + + + com/google/cloud/storage/Storage* + 7012 + * downloadTo(com.google.cloud.storage.BlobId, java.nio.file.Path, com.google.cloud.storage.Storage$BlobSourceOption[]) + 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 a19896995..79167aec8 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 @@ 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.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.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 @@ static Storage.BlobGetOption[] toGetOptions(BlobInfo blobInfo, BlobSourceOption. * @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 void downloadTo(Path path, BlobSourceOption... options) { * @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 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 634155f24..d27de9016 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.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.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 @@ Blob createFrom( */ ReadChannel reader(BlobId blob, BlobSourceOption... options); + /** + * Downloads the given blob to the given path using specified blob read options. + * + *
{@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
+   * }
+ * + * @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. + * + *
{@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
+   * }
+   * }
+ * + * @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 874d997eb..bf372c881 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.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.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.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 @@ public ReadChannel reader(BlobId blob, BlobSourceOption... options) { 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 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 targetOptions = BlobTargetOption.convert(blobInfo, options); 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 555349752..97a9b7e23 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.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.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 void testBuilder() { 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() { - @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() { - @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() { - @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 5619e5f7b..ae3682414 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 @@ 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.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.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 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 void testBucketLifecycleRules() { 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() { + @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() { + @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() { + @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 d755d38d2..d5aa1b6d4 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 void testGetBlobRawInput() throws IOException { 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 void testUploadFromDownloadTo() throws Exception { 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); }