Skip to main content

google_cloud_storage/storage/
open_object.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::Result;
16use crate::model_ext::{KeyAes256, OpenObjectRequest, ReadRange};
17use crate::object_descriptor::ObjectDescriptor;
18use crate::read_object::ReadObjectResponse;
19use crate::read_resume_policy::ReadResumePolicy;
20use crate::request_options::RequestOptions;
21use std::sync::Arc;
22use std::time::Duration;
23
24/// A request builder for [Storage::open_object][crate::client::Storage::open_object].
25///
26/// # Example
27/// ```
28/// use google_cloud_storage::client::Storage;
29/// # use google_cloud_storage::builder::storage::OpenObject;
30/// async fn sample(client: &Storage) -> anyhow::Result<()> {
31///     let builder: OpenObject = client
32///         .open_object("projects/_/buckets/my-bucket", "my-object");
33///     let descriptor = builder
34///         .set_generation(123)
35///         .send()
36///         .await?;
37///     println!("object metadata={:?}", descriptor.object());
38///     // Use `descriptor` to read data from `my-object`.
39///     Ok(())
40/// }
41/// ```
42#[derive(Clone, Debug)]
43pub struct OpenObject<S = crate::storage::transport::Storage> {
44    stub: Arc<S>,
45    request: OpenObjectRequest,
46    options: RequestOptions,
47}
48
49impl<S> OpenObject<S>
50where
51    S: crate::storage::stub::Storage + 'static,
52{
53    /// Sends the request, returning a new object descriptor.
54    ///
55    /// Example:
56    /// ```ignore
57    /// # use google_cloud_storage::{model_ext::KeyAes256, client::Storage};
58    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
59    /// let open = client
60    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
61    ///     .send()
62    ///     .await?;
63    /// println!("object metadata={:?}", open.object());
64    /// # Ok(()) }
65    /// ```
66    pub async fn send(self) -> Result<ObjectDescriptor> {
67        let (descriptor, _) = self.stub.open_object(self.request, self.options).await?;
68        Ok(descriptor)
69    }
70
71    /// Sends the request, returning a new object descriptor and reader.
72    ///
73    /// Example:
74    /// ```ignore
75    /// # use google_cloud_storage::client::Storage;
76    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
77    /// use google_cloud_storage::model_ext::ReadRange;
78    /// let (descriptor, mut reader) = client
79    ///     .open_object("projects/_/buckets/my-bucket", "my-object.parquet")
80    ///     .send_and_read(ReadRange::tail(32))
81    ///     .await?;
82    /// println!("object metadata={:?}", descriptor.object());
83    /// let data = reader.next().await.transpose()?;
84    /// # Ok(()) }
85    /// ```
86    ///
87    /// This method allows applications to open an object and issue a read
88    /// request in the same RPC, which is typically faster than opening an
89    /// object and then issuing a `read_range()` call. This may be useful when
90    /// opening objects that have metadata information in a footer or header.
91    pub async fn send_and_read(
92        mut self,
93        range: ReadRange,
94    ) -> Result<(ObjectDescriptor, ReadObjectResponse)> {
95        self.request.ranges.push(range);
96        let (descriptor, mut readers) = self.stub.open_object(self.request, self.options).await?;
97        if readers.len() == 1 {
98            return Ok((descriptor, readers.pop().unwrap()));
99        }
100        // Even if the service returns multiple read ranges, with different ids,
101        // the code in the library will return an error and close the stream.
102        unreachable!("the stub cannot create more readers")
103    }
104}
105
106impl<S> OpenObject<S> {
107    pub(crate) fn new<B, O>(
108        stub: std::sync::Arc<S>,
109        bucket: B,
110        object: O,
111        options: RequestOptions,
112    ) -> Self
113    where
114        B: Into<String>,
115        O: Into<String>,
116    {
117        let request = OpenObjectRequest::default()
118            .set_bucket(bucket)
119            .set_object(object);
120        Self {
121            request,
122            options,
123            stub,
124        }
125    }
126
127    /// Enables computation of MD5 checksums.
128    ///
129    /// By default, MD5 checksum validation is disabled. The SDK enables CRC32C validation by default
130    /// instead.
131    ///
132    /// # Example
133    /// ```
134    /// # use google_cloud_storage::client::Storage;
135    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
136    /// use google_cloud_storage::model_ext::ReadRange;
137    /// let builder = client
138    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
139    ///     .compute_md5();
140    /// let (descriptor, mut reader) = builder
141    ///     .send_and_read(ReadRange::all())
142    ///     .await?;
143    /// let mut contents = Vec::new();
144    /// while let Some(chunk) = reader.next().await.transpose()? {
145    ///     contents.extend_from_slice(&chunk);
146    /// }
147    /// println!("object contents={:?}", contents);
148    /// # Ok(()) }
149    /// ```
150    pub fn compute_md5(mut self) -> Self {
151        self.options.checksum.md5_hash = Some(crate::storage::checksum::details::Md5::default());
152        self
153    }
154
155    /// Enables computation of CRC32C checksums.
156    ///
157    /// Note that the library computes and verifies (if available) rolling CRC32C checksums at the end of
158    /// the download. Use `compute_crc32c(false)` to disable this object-level rolling computation, but note
159    /// that this reduces the data integrity guarantees. Data *can* be corrupted even when
160    /// downloaded over HTTPS or other encrypted channels.
161    ///
162    /// Note: Chunk-level CRC32C validation (validating each individual gRPC message as it arrives)
163    /// remains permanently enabled for maximum safety, as it is hardware-accelerated and adds negligible latency.
164    ///
165    /// # Example
166    /// ```
167    /// # use google_cloud_storage::client::Storage;
168    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
169    /// use google_cloud_storage::model_ext::ReadRange;
170    /// let builder = client
171    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
172    ///     .compute_crc32c(false);
173    /// let (descriptor, mut reader) = builder
174    ///     .send_and_read(ReadRange::all())
175    ///     .await?;
176    /// let mut contents = Vec::new();
177    /// while let Some(chunk) = reader.next().await.transpose()? {
178    ///     contents.extend_from_slice(&chunk);
179    /// }
180    /// println!("object contents={:?}", contents);
181    /// # Ok(()) }
182    /// ```
183    pub fn compute_crc32c(mut self, enable: bool) -> Self {
184        self.options.checksum.crc32c =
185            enable.then(crate::storage::checksum::details::Crc32c::default);
186        self
187    }
188
189    /// If present, selects a specific revision of this object (as
190    /// opposed to the latest version, the default).
191    ///
192    /// # Example
193    /// ```
194    /// # use google_cloud_storage::client::Storage;
195    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
196    /// use google_cloud_storage::read_resume_policy::{AlwaysResume, ReadResumePolicyExt};
197    /// let response = client
198    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
199    ///     .set_generation(123456)
200    ///     .send()
201    ///     .await?;
202    /// # Ok(()) }
203    /// ```
204    pub fn set_generation<T: Into<i64>>(mut self, v: T) -> Self {
205        self.request = self.request.set_generation(v.into());
206        self
207    }
208
209    /// Makes the operation conditional on whether the object's current generation
210    /// matches the given value.
211    ///
212    /// # Example
213    /// ```
214    /// # use google_cloud_storage::client::Storage;
215    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
216    /// use google_cloud_storage::read_resume_policy::{AlwaysResume, ReadResumePolicyExt};
217    /// let response = client
218    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
219    ///     .set_if_generation_match(123456)
220    ///     .send()
221    ///     .await?;
222    /// # Ok(()) }
223    /// ```
224    pub fn set_if_generation_match<T>(mut self, v: T) -> Self
225    where
226        T: Into<i64>,
227    {
228        self.request = self.request.set_if_generation_match(v.into());
229        self
230    }
231
232    /// Makes the operation conditional on whether the object's live generation
233    /// does not match the given value. If no live object exists, the precondition
234    /// fails.
235    ///
236    /// # Example
237    /// ```
238    /// # use google_cloud_storage::client::Storage;
239    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
240    /// use google_cloud_storage::read_resume_policy::{AlwaysResume, ReadResumePolicyExt};
241    /// let response = client
242    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
243    ///     .set_if_generation_not_match(123456)
244    ///     .send()
245    ///     .await?;
246    /// # Ok(()) }
247    /// ```
248    pub fn set_if_generation_not_match<T>(mut self, v: T) -> Self
249    where
250        T: Into<i64>,
251    {
252        self.request = self.request.set_if_generation_not_match(v.into());
253        self
254    }
255
256    /// Makes the operation conditional on whether the object's current
257    /// metageneration matches the given value.
258    ///
259    /// # Example
260    /// ```
261    /// # use google_cloud_storage::client::Storage;
262    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
263    /// use google_cloud_storage::read_resume_policy::{AlwaysResume, ReadResumePolicyExt};
264    /// let response = client
265    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
266    ///     .set_if_metageneration_match(123456)
267    ///     .send()
268    ///     .await?;
269    /// # Ok(()) }
270    /// ```
271    pub fn set_if_metageneration_match<T>(mut self, v: T) -> Self
272    where
273        T: Into<i64>,
274    {
275        self.request = self.request.set_if_metageneration_match(v.into());
276        self
277    }
278
279    /// Makes the operation conditional on whether the object's current
280    /// metageneration does not match the given value.
281    ///
282    /// # Example
283    /// ```
284    /// # use google_cloud_storage::client::Storage;
285    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
286    /// use google_cloud_storage::read_resume_policy::{AlwaysResume, ReadResumePolicyExt};
287    /// let response = client
288    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
289    ///     .set_if_metageneration_not_match(123456)
290    ///     .send()
291    ///     .await?;
292    /// # Ok(()) }
293    /// ```
294    pub fn set_if_metageneration_not_match<T>(mut self, v: T) -> Self
295    where
296        T: Into<i64>,
297    {
298        self.request = self.request.set_if_metageneration_not_match(v.into());
299        self
300    }
301
302    /// The encryption key used with the Customer-Supplied Encryption Keys
303    /// feature. In raw bytes format (not base64-encoded).
304    ///
305    /// Example:
306    /// ```
307    /// # use google_cloud_storage::{model_ext::KeyAes256, client::Storage};
308    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
309    /// let key: &[u8] = &[97; 32];
310    /// let response = client
311    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
312    ///     .set_key(KeyAes256::new(key)?)
313    ///     .send()
314    ///     .await?;
315    /// println!("response details={response:?}");
316    /// # Ok(()) }
317    /// ```
318    pub fn set_key(mut self, v: KeyAes256) -> Self {
319        self.request = self
320            .request
321            .set_common_object_request_params(crate::model::CommonObjectRequestParams::from(v));
322        self
323    }
324
325    /// The retry policy used for this request.
326    ///
327    /// # Example
328    /// ```
329    /// # use google_cloud_storage::client::Storage;
330    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
331    /// use google_cloud_storage::retry_policy::RetryableErrors;
332    /// use std::time::Duration;
333    /// use google_cloud_gax::retry_policy::RetryPolicyExt;
334    /// let response = client
335    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
336    ///     .with_retry_policy(
337    ///         RetryableErrors
338    ///             .with_attempt_limit(5)
339    ///             .with_time_limit(Duration::from_secs(10)),
340    ///     )
341    ///     .send()
342    ///     .await?;
343    /// println!("response details={response:?}");
344    /// # Ok(()) }
345    /// ```
346    pub fn with_retry_policy<V: Into<google_cloud_gax::retry_policy::RetryPolicyArg>>(
347        mut self,
348        v: V,
349    ) -> Self {
350        self.options.retry_policy = v.into().into();
351        self
352    }
353
354    /// The backoff policy used for this request.
355    ///
356    /// # Example
357    /// ```
358    /// # use google_cloud_storage::client::Storage;
359    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
360    /// use std::time::Duration;
361    /// use google_cloud_gax::exponential_backoff::ExponentialBackoff;
362    /// let response = client
363    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
364    ///     .with_backoff_policy(ExponentialBackoff::default())
365    ///     .send()
366    ///     .await?;
367    /// println!("response details={response:?}");
368    /// # Ok(()) }
369    /// ```
370    pub fn with_backoff_policy<V: Into<google_cloud_gax::backoff_policy::BackoffPolicyArg>>(
371        mut self,
372        v: V,
373    ) -> Self {
374        self.options.backoff_policy = v.into().into();
375        self
376    }
377
378    /// The retry throttler used for this request.
379    ///
380    /// Most of the time you want to use the same throttler for all the requests
381    /// in a client, and even the same throttler for many clients. Rarely it
382    /// may be necessary to use an custom throttler for some subset of the
383    /// requests.
384    ///
385    /// # Example
386    /// ```
387    /// # use google_cloud_storage::client::Storage;
388    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
389    /// let response = client
390    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
391    ///     .with_retry_throttler(adhoc_throttler())
392    ///     .send()
393    ///     .await?;
394    /// println!("response details={response:?}");
395    /// fn adhoc_throttler() -> google_cloud_gax::retry_throttler::SharedRetryThrottler {
396    ///     # panic!();
397    /// }
398    /// # Ok(()) }
399    /// ```
400    pub fn with_retry_throttler<V: Into<google_cloud_gax::retry_throttler::RetryThrottlerArg>>(
401        mut self,
402        v: V,
403    ) -> Self {
404        self.options.retry_throttler = v.into().into();
405        self
406    }
407
408    /// Configure the resume policy for read requests.
409    ///
410    /// The Cloud Storage client library can automatically resume a read that is
411    /// interrupted by a transient error. Applications may want to limit the
412    /// number of read attempts, or may wish to expand the type of errors
413    /// treated as retryable.
414    ///
415    /// # Example
416    /// ```
417    /// # use google_cloud_storage::client::Storage;
418    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
419    /// use google_cloud_storage::read_resume_policy::{AlwaysResume, ReadResumePolicyExt};
420    /// let response = client
421    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
422    ///     .with_read_resume_policy(AlwaysResume.with_attempt_limit(3))
423    ///     .send()
424    ///     .await?;
425    /// # Ok(()) }
426    /// ```
427    pub fn with_read_resume_policy<V>(mut self, v: V) -> Self
428    where
429        V: ReadResumePolicy + 'static,
430    {
431        self.options.set_read_resume_policy(std::sync::Arc::new(v));
432        self
433    }
434
435    /// Configure per-attempt timeout.
436    ///
437    /// # Example
438    /// ```
439    /// # use google_cloud_storage::client::Storage;
440    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
441    /// use std::time::Duration;
442    /// let response = client
443    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
444    ///     .with_attempt_timeout(Duration::from_secs(120))
445    ///     .send()
446    ///     .await?;
447    /// # Ok(()) }
448    /// ```
449    ///
450    /// The Cloud Storage client library times out `open_object()` attempts by
451    /// default (with a 60s timeout). Applications may want to set a different
452    /// value depending on how they are deployed.
453    ///
454    /// Note that the per-attempt timeout is subject to the overall retry loop
455    /// time limits (if any). The effective timeout for each attempt is the
456    /// smallest of (a) the per-attempt timeout, and (b) the remaining time in
457    /// the retry loop.
458    pub fn with_attempt_timeout(mut self, v: Duration) -> Self {
459        self.options.set_bidi_attempt_timeout(v);
460        self
461    }
462
463    /// Sets the `User-Agent` header for this request.
464    ///
465    /// # Example
466    /// ```
467    /// # use google_cloud_storage::client::Storage;
468    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
469    /// let mut response = client
470    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
471    ///     .with_user_agent("my-app/1.0.0")
472    ///     .send()
473    ///     .await?;
474    /// println!("response details={response:?}");
475    /// # Ok(()) }
476    /// ```
477    pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
478        self.options.user_agent = Some(user_agent.into());
479        self
480    }
481
482    /// Sets the project that will be billed for this request.
483    ///
484    /// Required for [Requester Pays] buckets. The value overrides any
485    /// `quota_project_id` configured on the credentials; the credential-level
486    /// header is suppressed for this RPC.
487    ///
488    /// # Example
489    /// ```
490    /// # use google_cloud_storage::client::Storage;
491    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
492    /// let response = client
493    ///     .open_object("projects/_/buckets/my-bucket", "my-object")
494    ///     .with_quota_project("my-billing-project")
495    ///     .send()
496    ///     .await?;
497    /// # Ok(()) }
498    /// ```
499    ///
500    /// [Requester Pays]: https://cloud.google.com/storage/docs/requester-pays
501    pub fn with_quota_project(mut self, project: impl Into<String>) -> Self {
502        self.options.set_quota_project(project);
503        self
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use crate::client::Storage;
511    use crate::model::{CommonObjectRequestParams, Object};
512    use crate::model_ext::tests::create_key_helper;
513    use anyhow::Result;
514    use gaxi::grpc::tonic::{Response as TonicResponse, Result as TonicResult};
515    use google_cloud_auth::credentials::anonymous::Builder as Anonymous;
516    use google_cloud_gax::retry_policy::NeverRetry;
517    use http::HeaderValue;
518    use static_assertions::assert_impl_all;
519    use storage_grpc_mock::google::storage::v2::{
520        BidiReadObjectResponse, ChecksummedData, Object as ProtoObject, ObjectRangeData,
521        ReadRange as ProtoRange,
522    };
523    use storage_grpc_mock::{MockStorage, start};
524
525    const BUCKET_NAME: &str = "projects/_/buckets/test-bucket";
526    const OBJECT_NAME: &str = "test-object";
527    const USER_AGENT: &str = "quick_foxes_lazy_dogs/1.2.3";
528    const BIND_ADDRESS: &str = "0.0.0.0:0";
529
530    // Verify `open_object()` meets normal Send, Sync, requirements.
531    #[tokio::test]
532    async fn traits() -> Result<()> {
533        assert_impl_all!(OpenObject: Clone, std::fmt::Debug, Send, Sync);
534
535        let client = Storage::builder()
536            .with_credentials(Anonymous::new().build())
537            .build()
538            .await?;
539
540        fn need_send<T: Send>(_val: &T) {}
541        fn need_static<T: 'static>(_val: &T) {}
542
543        let open = client.open_object(BUCKET_NAME, OBJECT_NAME);
544        need_static(&open);
545
546        let fut = client.open_object(BUCKET_NAME, OBJECT_NAME).send();
547        need_send(&fut);
548        need_static(&fut);
549        Ok(())
550    }
551
552    #[tokio::test]
553    async fn open_object_normal() -> Result<()> {
554        let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
555        let initial = BidiReadObjectResponse {
556            metadata: Some(ProtoObject {
557                bucket: BUCKET_NAME.to_string(),
558                name: OBJECT_NAME.to_string(),
559                generation: 123456,
560                size: 42,
561                ..ProtoObject::default()
562            }),
563            ..BidiReadObjectResponse::default()
564        };
565        tx.send(Ok(initial)).await?;
566
567        let mut mock = MockStorage::new();
568        mock.expect_bidi_read_object()
569            .return_once(|_| Ok(TonicResponse::from(rx)));
570        let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
571
572        let client = Storage::builder()
573            .with_endpoint(endpoint)
574            .with_credentials(Anonymous::new().build())
575            .build()
576            .await?;
577        let descriptor = client.open_object(BUCKET_NAME, OBJECT_NAME).send().await?;
578
579        let got = descriptor.object();
580        let want = Object::new()
581            .set_bucket(BUCKET_NAME)
582            .set_name(OBJECT_NAME)
583            .set_generation(123456)
584            .set_size(42);
585        assert_eq!(got, want);
586
587        Ok(())
588    }
589
590    #[tokio::test]
591    async fn attributes() -> Result<()> {
592        let options = RequestOptions::new();
593        let builder = OpenObject::new(
594            Arc::new(StorageStub),
595            BUCKET_NAME.to_string(),
596            OBJECT_NAME.to_string(),
597            options,
598        )
599        .set_generation(123)
600        .set_if_generation_match(234)
601        .set_if_generation_not_match(345)
602        .set_if_metageneration_match(456)
603        .set_if_metageneration_not_match(567);
604        let want = OpenObjectRequest::default()
605            .set_bucket(BUCKET_NAME)
606            .set_object(OBJECT_NAME)
607            .set_generation(123)
608            .set_if_generation_match(234)
609            .set_if_generation_not_match(345)
610            .set_if_metageneration_match(456)
611            .set_if_metageneration_not_match(567);
612        assert_eq!(builder.request, want);
613        Ok(())
614    }
615
616    #[tokio::test]
617    async fn csek() -> Result<()> {
618        let options = RequestOptions::new();
619        let builder = OpenObject::new(
620            Arc::new(StorageStub),
621            BUCKET_NAME.to_string(),
622            OBJECT_NAME.to_string(),
623            options,
624        );
625
626        let (raw_key, _, _, _) = create_key_helper();
627        let key = KeyAes256::new(&raw_key)?;
628        let builder = builder.set_key(key.clone());
629        let want = OpenObjectRequest::default()
630            .set_bucket(BUCKET_NAME)
631            .set_object(OBJECT_NAME)
632            .set_common_object_request_params(CommonObjectRequestParams::from(key));
633        assert_eq!(builder.request, want);
634        Ok(())
635    }
636
637    #[tokio::test]
638    async fn request_options() -> Result<()> {
639        use crate::read_resume_policy::NeverResume;
640        use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
641        use google_cloud_gax::retry_policy::Aip194Strict;
642        use google_cloud_gax::retry_throttler::CircuitBreaker;
643
644        let options = RequestOptions::new();
645        let builder = OpenObject::new(
646            Arc::new(StorageStub),
647            BUCKET_NAME.to_string(),
648            OBJECT_NAME.to_string(),
649            options,
650        )
651        .with_backoff_policy(
652            ExponentialBackoffBuilder::default()
653                .with_scaling(4.0)
654                .build()
655                .expect("expontial backoff builds"),
656        )
657        .with_retry_policy(Aip194Strict)
658        .with_retry_throttler(CircuitBreaker::default())
659        .with_read_resume_policy(NeverResume)
660        .with_attempt_timeout(Duration::from_secs(120))
661        .with_user_agent(USER_AGENT);
662
663        let got = builder.options;
664        assert!(
665            format!("{:?}", got.backoff_policy).contains("ExponentialBackoff"),
666            "{got:?}"
667        );
668        assert!(
669            format!("{:?}", got.retry_policy).contains("Aip194Strict"),
670            "{got:?}"
671        );
672        assert!(
673            format!("{:?}", got.retry_throttler).contains("CircuitBreaker"),
674            "{got:?}"
675        );
676        assert!(
677            format!("{:?}", got.read_resume_policy()).contains("NeverResume"),
678            "{got:?}"
679        );
680        assert_eq!(
681            got.bidi_attempt_timeout,
682            Duration::from_secs(120),
683            "{got:?}"
684        );
685        assert_eq!(got.user_agent.as_deref(), Some(USER_AGENT), "{got:?}");
686
687        Ok(())
688    }
689
690    #[tokio::test]
691    async fn send() -> anyhow::Result<()> {
692        let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
693        let initial = BidiReadObjectResponse {
694            metadata: Some(ProtoObject {
695                bucket: BUCKET_NAME.to_string(),
696                name: OBJECT_NAME.to_string(),
697                generation: 123456,
698                ..ProtoObject::default()
699            }),
700            ..BidiReadObjectResponse::default()
701        };
702        tx.send(Ok(initial)).await?;
703
704        let mut mock = MockStorage::new();
705        mock.expect_bidi_read_object()
706            .return_once(|_| Ok(TonicResponse::from(rx)));
707        let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
708
709        let client = Storage::builder()
710            .with_credentials(Anonymous::new().build())
711            .with_endpoint(endpoint)
712            .build()
713            .await?;
714
715        let descriptor = client.open_object(BUCKET_NAME, OBJECT_NAME).send().await?;
716        let want = Object::new()
717            .set_bucket(BUCKET_NAME)
718            .set_name(OBJECT_NAME)
719            .set_generation(123456);
720        assert_eq!(descriptor.object(), want, "{descriptor:?}");
721        assert_eq!(
722            descriptor.headers().get("content-type"),
723            Some(&HeaderValue::from_static("application/grpc")),
724            "headers={:?}",
725            descriptor.headers()
726        );
727        Ok(())
728    }
729
730    #[tokio::test]
731    async fn send_and_read() -> anyhow::Result<()> {
732        let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
733        let payload = Vec::from_iter((0..32).map(|i| i as u8));
734        let initial = BidiReadObjectResponse {
735            metadata: Some(ProtoObject {
736                bucket: BUCKET_NAME.to_string(),
737                name: OBJECT_NAME.to_string(),
738                generation: 123456,
739                ..ProtoObject::default()
740            }),
741            object_data_ranges: vec![ObjectRangeData {
742                read_range: Some(ProtoRange {
743                    read_id: 0_i64,
744                    ..ProtoRange::default()
745                }),
746                range_end: true,
747                checksummed_data: Some(ChecksummedData {
748                    content: payload.clone(),
749                    crc32c: None,
750                }),
751            }],
752            ..BidiReadObjectResponse::default()
753        };
754        tx.send(Ok(initial)).await?;
755
756        let mut mock = MockStorage::new();
757        mock.expect_bidi_read_object()
758            .return_once(|_| Ok(TonicResponse::from(rx)));
759        let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
760
761        let client = Storage::builder()
762            .with_credentials(Anonymous::new().build())
763            .with_endpoint(endpoint)
764            .build()
765            .await?;
766
767        let (descriptor, mut reader) = client
768            .open_object(BUCKET_NAME, OBJECT_NAME)
769            .send_and_read(ReadRange::tail(32))
770            .await?;
771        let want = Object::new()
772            .set_bucket(BUCKET_NAME)
773            .set_name(OBJECT_NAME)
774            .set_generation(123456);
775        assert_eq!(descriptor.object(), want, "{descriptor:?}");
776        assert_eq!(
777            descriptor.headers().get("content-type"),
778            Some(&HeaderValue::from_static("application/grpc")),
779            "headers={:?}",
780            descriptor.headers()
781        );
782
783        let mut got_payload = Vec::new();
784        while let Some(chunk) = reader.next().await.transpose()? {
785            got_payload.extend_from_slice(&chunk);
786        }
787        assert_eq!(got_payload, payload);
788        Ok(())
789    }
790
791    #[tokio::test]
792    async fn send_and_read_checksum_bypassed_due_to_ranged_read() -> anyhow::Result<()> {
793        let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
794        let payload = Vec::from_iter((0..32).map(|i| i as u8));
795        let expected_checksum = crc32c::crc32c(&payload);
796        let corrupted_checksum = expected_checksum + 1; // Corrupted!
797        let initial = BidiReadObjectResponse {
798            metadata: Some(ProtoObject {
799                bucket: BUCKET_NAME.to_string(),
800                name: OBJECT_NAME.to_string(),
801                generation: 123456,
802                checksums: Some(storage_grpc_mock::google::storage::v2::ObjectChecksums {
803                    crc32c: Some(corrupted_checksum),
804                    ..Default::default()
805                }),
806                ..ProtoObject::default()
807            }),
808            object_data_ranges: vec![ObjectRangeData {
809                read_range: Some(ProtoRange {
810                    read_id: 0_i64,
811                    ..ProtoRange::default()
812                }),
813                range_end: true,
814                checksummed_data: Some(ChecksummedData {
815                    content: payload.clone(),
816                    crc32c: None,
817                }),
818            }],
819            ..BidiReadObjectResponse::default()
820        };
821        tx.send(Ok(initial)).await?;
822
823        let mut mock = MockStorage::new();
824        mock.expect_bidi_read_object()
825            .return_once(|_| Ok(TonicResponse::from(rx)));
826        let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
827
828        let client = Storage::builder()
829            .with_credentials(Anonymous::new().build())
830            .with_endpoint(endpoint)
831            .build()
832            .await?;
833
834        let (_descriptor, mut reader) = client
835            .open_object(BUCKET_NAME, OBJECT_NAME)
836            // Even though the client has checksums enabled by default,
837            // a ranged read will explicitly bypass the validation!
838            .send_and_read(ReadRange::segment(0, 32))
839            .await?;
840
841        let mut got_payload = Vec::new();
842        while let Some(res) = reader.next().await {
843            match res {
844                Ok(chunk) => got_payload.extend_from_slice(&chunk),
845                Err(e) => {
846                    panic!("Did not expect error on ranged read: {e}");
847                }
848            }
849        }
850        assert_eq!(got_payload, payload);
851        Ok(())
852    }
853    #[tokio::test]
854    async fn send_and_read_checksum_mismatch() -> anyhow::Result<()> {
855        let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
856        let payload = Vec::from_iter((0..32).map(|i| i as u8));
857        let expected_checksum = crc32c::crc32c(&payload);
858        let corrupted_checksum = expected_checksum + 1; // Corrupted!
859        let initial = BidiReadObjectResponse {
860            metadata: Some(ProtoObject {
861                bucket: BUCKET_NAME.to_string(),
862                name: OBJECT_NAME.to_string(),
863                generation: 123456,
864                checksums: Some(storage_grpc_mock::google::storage::v2::ObjectChecksums {
865                    crc32c: Some(corrupted_checksum),
866                    ..Default::default()
867                }),
868                ..ProtoObject::default()
869            }),
870            object_data_ranges: vec![ObjectRangeData {
871                read_range: Some(ProtoRange {
872                    read_id: 0_i64,
873                    ..ProtoRange::default()
874                }),
875                range_end: true,
876                checksummed_data: Some(ChecksummedData {
877                    content: payload.clone(),
878                    crc32c: None,
879                }),
880            }],
881            ..BidiReadObjectResponse::default()
882        };
883        tx.send(Ok(initial)).await?;
884
885        let mut mock = MockStorage::new();
886        mock.expect_bidi_read_object()
887            .return_once(|_| Ok(TonicResponse::from(rx)));
888        let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
889
890        let client = Storage::builder()
891            .with_credentials(Anonymous::new().build())
892            .with_endpoint(endpoint)
893            .build()
894            .await?;
895
896        let (_descriptor, mut reader) = client
897            .open_object(BUCKET_NAME, OBJECT_NAME)
898            .send_and_read(ReadRange::all())
899            .await?;
900
901        let mut got_payload = Vec::new();
902        let mut got_err = false;
903        while let Some(res) = reader.next().await {
904            match res {
905                Ok(chunk) => got_payload.extend_from_slice(&chunk),
906                Err(e) => {
907                    use std::error::Error as _;
908                    got_err = true;
909                    let source = e
910                        .source()
911                        .and_then(|err| err.downcast_ref::<crate::error::ReadError>());
912                    assert!(
913                        matches!(source, Some(crate::error::ReadError::ChecksumMismatch(_))),
914                        "Expected ChecksumMismatch, got: {source:?}"
915                    );
916                }
917            }
918        }
919        assert!(got_err, "Expected a checksum mismatch error");
920        assert_eq!(got_payload, payload);
921        Ok(())
922    }
923
924    #[tokio::test(start_paused = true)]
925    async fn timeout() -> anyhow::Result<()> {
926        let (_tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
927
928        let mut mock = MockStorage::new();
929        mock.expect_bidi_read_object()
930            .return_once(|_| Ok(TonicResponse::from(rx)));
931        let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
932
933        let client = Storage::builder()
934            .with_credentials(Anonymous::new().build())
935            .with_endpoint(endpoint)
936            .with_retry_policy(NeverRetry)
937            .build()
938            .await?;
939
940        // This will timeout because we never send the initial message over `_tx`.
941        let target = Duration::from_secs(120);
942        let start = tokio::time::Instant::now();
943        let err = client
944            .open_object(BUCKET_NAME, OBJECT_NAME)
945            .with_attempt_timeout(target)
946            .send()
947            .await
948            .unwrap_err();
949        assert!(err.is_timeout(), "{err:?}");
950        assert_eq!(start.elapsed(), target);
951
952        Ok(())
953    }
954
955    #[tokio::test]
956    async fn user_agent() -> anyhow::Result<()> {
957        let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
958        let initial = BidiReadObjectResponse {
959            metadata: Some(ProtoObject {
960                bucket: BUCKET_NAME.to_string(),
961                name: OBJECT_NAME.to_string(),
962                generation: 123456,
963                ..ProtoObject::default()
964            }),
965            ..BidiReadObjectResponse::default()
966        };
967        tx.send(Ok(initial)).await?;
968
969        let mut mock = MockStorage::new();
970        mock.expect_bidi_read_object().return_once(|request| {
971            let metadata = request.metadata();
972            let user_agent = metadata
973                .get(http::header::USER_AGENT.as_str())
974                .and_then(|v| v.to_str().ok())
975                .expect("user-agent should be set");
976
977            let got = user_agent.split(' ').any(|s| s == USER_AGENT);
978            assert!(got, "{user_agent:?}");
979
980            Ok(TonicResponse::from(rx))
981        });
982        let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
983
984        let client = Storage::builder()
985            .with_credentials(Anonymous::new().build())
986            .with_endpoint(endpoint)
987            .build()
988            .await?;
989
990        let _descriptor = client
991            .open_object(BUCKET_NAME, OBJECT_NAME)
992            .with_user_agent(USER_AGENT)
993            .send()
994            .await?;
995        Ok(())
996    }
997
998    #[test]
999    fn checksum_toggles() -> Result<()> {
1000        let options = RequestOptions::new();
1001
1002        // By default, crc32c is enabled and md5 is disabled.
1003        assert!(options.checksum.crc32c.is_some());
1004        assert!(options.checksum.md5_hash.is_none());
1005
1006        let builder = OpenObject::new(
1007            Arc::new(StorageStub),
1008            BUCKET_NAME.to_string(),
1009            OBJECT_NAME.to_string(),
1010            options,
1011        )
1012        .compute_crc32c(false)
1013        .compute_md5();
1014
1015        let got = builder.options;
1016        assert!(got.checksum.crc32c.is_none());
1017        assert!(got.checksum.md5_hash.is_some());
1018
1019        Ok(())
1020    }
1021
1022    #[tokio::test]
1023    async fn quota_project() -> anyhow::Result<()> {
1024        const PROJECT_NAME: &str = "project_lazy_dog";
1025        let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
1026        let initial = BidiReadObjectResponse {
1027            metadata: Some(ProtoObject {
1028                bucket: BUCKET_NAME.to_string(),
1029                name: OBJECT_NAME.to_string(),
1030                generation: 123456,
1031                ..ProtoObject::default()
1032            }),
1033            ..BidiReadObjectResponse::default()
1034        };
1035        tx.send(Ok(initial)).await?;
1036
1037        let mut mock = MockStorage::new();
1038        mock.expect_bidi_read_object().return_once(|request| {
1039            let user_project = request
1040                .metadata()
1041                .get("x-goog-user-project")
1042                .and_then(|v| v.to_str().ok())
1043                .expect("x-goog-user-project should be set");
1044            assert_eq!(user_project, PROJECT_NAME);
1045            Ok(TonicResponse::from(rx))
1046        });
1047        let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
1048
1049        let client = Storage::builder()
1050            .with_credentials(Anonymous::new().build())
1051            .with_endpoint(endpoint)
1052            .build()
1053            .await?;
1054
1055        let _descriptor = client
1056            .open_object(BUCKET_NAME, OBJECT_NAME)
1057            .with_quota_project(PROJECT_NAME)
1058            .send()
1059            .await?;
1060        Ok(())
1061    }
1062
1063    #[derive(Debug)]
1064    struct StorageStub;
1065    impl crate::stub::Storage for StorageStub {}
1066}