diff --git a/Cargo.lock b/Cargo.lock index f101acdb48..768faac9bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4403,7 +4403,7 @@ dependencies = [ [[package]] name = "google-cloud-storage" -version = "1.1.0" +version = "1.2.0" dependencies = [ "anyhow", "async-trait", diff --git a/src/integration-tests/src/storage/read_gzip.rs b/src/integration-tests/src/storage/read_gzip.rs index 294a78e4c4..9ca88799b0 100644 --- a/src/integration-tests/src/storage/read_gzip.rs +++ b/src/integration-tests/src/storage/read_gzip.rs @@ -88,6 +88,31 @@ pub async fn test(bucket: &storage::model::Bucket) -> anyhow::Result<()> { let got = super::read_all(response).await?; assert_eq!(got, compressed); + tracing::info!("Reading decompressed object"); + let response = client + .read_object(&object.bucket, &object.name) + .set_generation(object.generation) + .with_automatic_decompression(true) + .send() + .await?; + let highlights = response.object(); + tracing::info!("Decompressed object read: {:?}", highlights); + assert_eq!(highlights.content_encoding, "gzip", "{highlights:?}"); + assert_eq!(highlights.content_type, "text/plain", "{highlights:?}"); + assert_eq!(highlights.size as usize, compressed.len(), "{highlights:?}"); + assert_eq!(highlights.generation, object.generation, "{highlights:?}"); + assert_eq!( + highlights.metageneration, object.metageneration, + "{highlights:?}" + ); + assert_eq!(highlights.checksums, object.checksums, "{highlights:?}"); + assert_eq!( + highlights.storage_class, object.storage_class, + "{highlights:?}" + ); + let got = super::read_all(response).await?; + assert_eq!(String::from_utf8(got), Ok(CONTENT.to_string())); + tracing::info!("Reading compressed object head"); let response = client .read_object(&object.bucket, &object.name) diff --git a/src/storage/Cargo.toml b/src/storage/Cargo.toml index 4430a3bdbf..dd36c152bd 100644 --- a/src/storage/Cargo.toml +++ b/src/storage/Cargo.toml @@ -14,7 +14,7 @@ [package] name = "google-cloud-storage" -version = "1.1.0" +version = "1.2.0" description = "Google Cloud Client Libraries for Rust - Storage" # Inherit other attributes from the workspace. edition.workspace = true diff --git a/src/storage/src/storage/read_object.rs b/src/storage/src/storage/read_object.rs index 768af388ab..2a9e5f7ae0 100644 --- a/src/storage/src/storage/read_object.rs +++ b/src/storage/src/storage/read_object.rs @@ -360,6 +360,33 @@ where self } + /// Enables automatic decompression. + /// + /// The Cloud Storage service [automatically decompresses] objects + /// with `content_encoding == "gzip"` during reads. The client library + /// disables this behavior by default, as it is not possible to + /// perform ranged reads or to resume interrupted downloads if automatic + /// decompression is enabled. + /// + /// Use this option to enable automatic decompression. + /// + /// # Example + /// ``` + /// # use google_cloud_storage::client::Storage; + /// # async fn sample(client: &Storage) -> anyhow::Result<()> { + /// let response = client + /// .read_object("projects/_/buckets/my-bucket", "my-object") + /// .with_automatic_decompression(true) + /// .send() + /// .await?; + /// println!("response details={response:?}"); + /// # Ok(()) } + /// ``` + pub fn with_automatic_decompression(mut self, v: bool) -> Self { + self.options.automatic_decompression = v; + self + } + /// Sends the request. pub async fn send(self) -> Result { self.stub.read_object(self.request, self.options).await @@ -429,16 +456,21 @@ impl Reader { .header( "x-goog-api-client", reqwest::header::HeaderValue::from_static(&self::info::X_GOOG_API_CLIENT_HEADER), - ) + ); + + let builder = if self.options.automatic_decompression { + builder + } else { // Disable decompressive transcoding: https://cloud.google.com/storage/docs/transcoding // // The default is to decompress objects that have `contentEncoding == "gzip"`. This header // tells Cloud Storage to disable automatic decompression. It has no effect on objects // with a different `contentEncoding` value. - .header( + builder.header( "accept-encoding", reqwest::header::HeaderValue::from_static("gzip"), - ); + ) + }; // Add the optional query parameters. let builder = if self.request.generation != 0 { @@ -997,6 +1029,31 @@ mod tests { Ok(()) } + #[tokio::test] + async fn read_object_automatic_decompression_headers() -> Result { + // The API takes the unencoded byte array. + let inner = test_inner_client(test_builder()); + let stub = crate::storage::transport::Storage::new(inner.clone()); + let builder = ReadObject::new( + stub, + "projects/_/buckets/bucket", + "object", + inner.options.clone(), + ) + .with_automatic_decompression(true); + let request = http_request_builder(inner, builder).await?.build()?; + + assert_eq!(request.method(), reqwest::Method::GET); + assert_eq!( + request.url().as_str(), + "http://private.googleapis.com/storage/v1/b/bucket/o/object?alt=media" + ); + + let headers = request.headers(); + assert!(headers.get("accept-encoding").is_none(), "{request:?}"); + Ok(()) + } + #[tokio::test] async fn read_object_encryption_headers() -> Result { // Make a 32-byte key. diff --git a/src/storage/src/storage/request_options.rs b/src/storage/src/storage/request_options.rs index 66bb328f49..cc9473f34d 100644 --- a/src/storage/src/storage/request_options.rs +++ b/src/storage/src/storage/request_options.rs @@ -35,6 +35,7 @@ pub struct RequestOptions { pub(crate) resumable_upload_buffer_size: usize, pub(crate) idempotency: Option, pub(crate) checksum: Checksum, + pub(crate) automatic_decompression: bool, } const MIB: usize = 1024 * 1024_usize; @@ -61,6 +62,7 @@ impl RequestOptions { crc32c: Some(Crc32c::default()), md5_hash: None, }, + automatic_decompression: false, } } }