1mod non_resumable;
16mod parse_http_response;
17mod resumable;
18
19use super::client::*;
20use super::*;
21use crate::model_ext::KeyAes256;
22use crate::read_object::ReadObjectResponse;
23use crate::read_resume_policy::ReadResumePolicy;
24use crate::storage::checksum::details::Md5;
25use crate::storage::request_options::RequestOptions;
26use gaxi::attempt_info::AttemptInfo;
27use gaxi::http::HttpRequestBuilder;
28use gaxi::http::reqwest::{HeaderValue, Method, Response};
29use google_cloud_gax::options::internal::{PathTemplate, RequestOptionsExt, ResourceName};
30
31#[derive(Debug)]
68pub struct ReadObject<S = crate::storage::transport::Storage>
69where
70 S: crate::storage::stub::Storage + 'static,
71{
72 stub: std::sync::Arc<S>,
73 request: crate::model::ReadObjectRequest,
74 options: RequestOptions,
75}
76
77impl<S> Clone for ReadObject<S>
78where
79 S: crate::storage::stub::Storage + 'static,
80{
81 fn clone(&self) -> Self {
82 Self {
83 stub: self.stub.clone(),
84 request: self.request.clone(),
85 options: self.options.clone(),
86 }
87 }
88}
89
90impl<S> ReadObject<S>
91where
92 S: crate::storage::stub::Storage + 'static,
93{
94 pub(crate) fn new<B, O>(
95 stub: std::sync::Arc<S>,
96 bucket: B,
97 object: O,
98 options: RequestOptions,
99 ) -> Self
100 where
101 B: Into<String>,
102 O: Into<String>,
103 {
104 ReadObject {
105 stub,
106 request: crate::model::ReadObjectRequest::new()
107 .set_bucket(bucket)
108 .set_object(object),
109 options,
110 }
111 }
112
113 pub fn compute_md5(self) -> Self {
142 let mut this = self;
143 this.options.checksum.md5_hash = Some(Md5::default());
144 this
145 }
146
147 pub fn compute_crc32c(mut self, enable: bool) -> Self {
172 if enable {
173 self.options
174 .checksum
175 .crc32c
176 .get_or_insert_with(Default::default);
177 } else {
178 self.options.checksum.crc32c = None;
179 }
180 self
181 }
182
183 pub fn set_generation<T: Into<i64>>(mut self, v: T) -> Self {
186 self.request.generation = v.into();
187 self
188 }
189
190 pub fn set_if_generation_match<T>(mut self, v: T) -> Self
194 where
195 T: Into<i64>,
196 {
197 self.request.if_generation_match = Some(v.into());
198 self
199 }
200
201 pub fn set_if_generation_not_match<T>(mut self, v: T) -> Self
206 where
207 T: Into<i64>,
208 {
209 self.request.if_generation_not_match = Some(v.into());
210 self
211 }
212
213 pub fn set_if_metageneration_match<T>(mut self, v: T) -> Self
216 where
217 T: Into<i64>,
218 {
219 self.request.if_metageneration_match = Some(v.into());
220 self
221 }
222
223 pub fn set_if_metageneration_not_match<T>(mut self, v: T) -> Self
226 where
227 T: Into<i64>,
228 {
229 self.request.if_metageneration_not_match = Some(v.into());
230 self
231 }
232
233 pub fn set_read_range(mut self, range: crate::model_ext::ReadRange) -> Self {
284 self.request.with_range(range);
285 self
286 }
287
288 pub fn set_key(mut self, v: KeyAes256) -> Self {
305 self.request.common_object_request_params = Some(v.into());
306 self
307 }
308
309 pub fn with_retry_policy<V: Into<google_cloud_gax::retry_policy::RetryPolicyArg>>(
331 mut self,
332 v: V,
333 ) -> Self {
334 self.options.retry_policy = v.into().into();
335 self
336 }
337
338 pub fn with_backoff_policy<V: Into<google_cloud_gax::backoff_policy::BackoffPolicyArg>>(
355 mut self,
356 v: V,
357 ) -> Self {
358 self.options.backoff_policy = v.into().into();
359 self
360 }
361
362 pub fn with_retry_throttler<V: Into<google_cloud_gax::retry_throttler::RetryThrottlerArg>>(
385 mut self,
386 v: V,
387 ) -> Self {
388 self.options.retry_throttler = v.into().into();
389 self
390 }
391
392 pub fn with_read_resume_policy<V>(mut self, v: V) -> Self
412 where
413 V: ReadResumePolicy + 'static,
414 {
415 self.options.set_read_resume_policy(std::sync::Arc::new(v));
416 self
417 }
418
419 pub fn with_automatic_decompression(mut self, v: bool) -> Self {
442 self.options.automatic_decompression = v;
443 self
444 }
445
446 pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
461 self.options.user_agent = Some(user_agent.into());
462 self
463 }
464
465 pub fn with_quota_project(mut self, project: impl Into<String>) -> Self {
485 self.options.set_quota_project(project);
486 self
487 }
488
489 pub async fn send(self) -> Result<ReadObjectResponse> {
491 self.stub.read_object(self.request, self.options).await
492 }
493}
494
495#[derive(Clone, Debug)]
497pub(crate) struct Reader {
498 pub inner: std::sync::Arc<StorageInner>,
499 pub request: crate::model::ReadObjectRequest,
500 pub options: RequestOptions,
501}
502
503impl Reader {
504 async fn read(self) -> Result<Response> {
505 let throttler = self.options.retry_throttler.clone();
506 let retry = self.options.retry_policy.clone();
507 let backoff = self.options.backoff_policy.clone();
508 let mut count = 0;
509 let inner = async move |_| {
510 let current = count;
511 count += 1;
512 self.read_attempt(current).await
513 };
514
515 google_cloud_gax::retry_loop_internal::retry_loop(
516 inner,
517 async |duration| tokio::time::sleep(duration).await,
518 true,
519 throttler,
520 retry,
521 backoff,
522 )
523 .await
524 }
525
526 async fn read_attempt(&self, attempt_count: u32) -> Result<Response> {
527 let builder = self.http_request_builder().await?;
528 let options = self
529 .options
530 .gax()
531 .insert_extension(PathTemplate("/storage/v1/b/{bucket}/o/{object}"))
532 .insert_extension(ResourceName(format!(
533 "//storage.googleapis.com/{}",
534 self.request.bucket
535 )));
536 let response = builder
537 .send(options, AttemptInfo::new(attempt_count))
538 .await?;
539 if !response.status().is_success() {
540 return gaxi::http::to_http_error(response).await;
541 }
542 Ok(response)
543 }
544
545 async fn http_request_builder(&self) -> Result<HttpRequestBuilder> {
546 let bucket = &self.request.bucket;
548 let bucket_id = bucket
549 .as_str()
550 .strip_prefix("projects/_/buckets/")
551 .ok_or_else(|| {
552 Error::binding(format!(
553 "malformed bucket name, it must start with `projects/_/buckets/`: {bucket}"
554 ))
555 })?;
556 let object = &self.request.object;
557
558 let builder = self
560 .inner
561 .client
562 .http_builder(
563 Method::GET,
564 &format!("/storage/v1/b/{bucket_id}/o/{}", enc(object)),
565 )
566 .query("alt", "media")
567 .header(
568 "x-goog-api-client",
569 HeaderValue::from_static(&self::info::X_GOOG_API_CLIENT_HEADER),
570 );
571
572 let builder = if self.options.automatic_decompression {
573 builder
574 } else {
575 builder.header("accept-encoding", HeaderValue::from_static("gzip"))
581 };
582
583 let builder = if self.request.generation != 0 {
585 builder.query("generation", self.request.generation)
586 } else {
587 builder
588 };
589 let builder = self
590 .request
591 .if_generation_match
592 .iter()
593 .fold(builder, |b, v| b.query("ifGenerationMatch", v));
594 let builder = self
595 .request
596 .if_generation_not_match
597 .iter()
598 .fold(builder, |b, v| b.query("ifGenerationNotMatch", v));
599 let builder = self
600 .request
601 .if_metageneration_match
602 .iter()
603 .fold(builder, |b, v| b.query("ifMetagenerationMatch", v));
604 let builder = self
605 .request
606 .if_metageneration_not_match
607 .iter()
608 .fold(builder, |b, v| b.query("ifMetagenerationNotMatch", v));
609
610 let builder = apply_customer_supplied_encryption_headers(
611 builder,
612 &self.request.common_object_request_params,
613 );
614
615 let builder = match (self.request.read_offset, self.request.read_limit) {
617 (_, l) if l < 0 => {
619 unreachable!("ReadObject build never sets a negative read_limit value")
620 }
621 (o, l) if o < 0 && l > 0 => unreachable!(
623 "ReadObject builder never sets a positive read_offset value with a negative read_limit value"
624 ),
625 (0, 0) => builder,
627 (o, 0) if o < 0 => builder.header("range", format!("bytes={o}")),
629 (o, 0) => builder.header("range", format!("bytes={o}-")),
632 (o, l) => builder.header("range", format!("bytes={o}-{}", o + l - 1)),
635 };
636
637 Ok(builder)
638 }
639
640 fn is_gunzipped(response: &Response) -> bool {
641 const TRANSFORMATION: &str = "x-guploader-response-body-transformations";
654 use http::header::WARNING;
655 if response
656 .headers()
657 .get(TRANSFORMATION)
658 .is_some_and(|h| h.as_bytes() == "gunzipped".as_bytes())
659 {
660 return true;
661 }
662 response
663 .headers()
664 .get(WARNING)
665 .is_some_and(|h| h.as_bytes() == "214 UploadServer gunzipped".as_bytes())
666 }
667
668 pub(crate) async fn response(self) -> Result<ReadObjectResponse> {
669 let response = self.clone().read().await?;
670 if Self::is_gunzipped(&response) {
671 return Ok(ReadObjectResponse::new(Box::new(
672 non_resumable::NonResumableResponse::new(response)?,
673 )));
674 }
675 Ok(ReadObjectResponse::new(Box::new(
676 resumable::ResumableResponse::new(self, response)?,
677 )))
678 }
679}
680
681#[cfg(test)]
682mod resume_tests;
683
684#[cfg(test)]
685mod tests {
686 use super::client::tests::{test_builder, test_inner_client};
687 use super::*;
688 use crate::error::{ChecksumMismatch, ReadError};
689 use crate::model_ext::{KeyAes256, ReadRange, tests::create_key_helper};
690 use base64::Engine;
691 use futures::TryStreamExt;
692 use google_cloud_auth::credentials::{
693 CacheableResource, Credentials, EntityTag, anonymous::Builder as Anonymous,
694 testing::error_credentials,
695 };
696 use httptest::{Expectation, Server, all_of, cycle, matchers::*, responders::status_code};
697 use pretty_assertions::assert_eq;
698 use std::collections::HashMap;
699 use std::error::Error;
700 use std::sync::Arc;
701 use test_case::test_case;
702
703 type Result = anyhow::Result<()>;
704
705 mockall::mock! {
706 #[derive(Debug)]
707 Credentials {}
708 impl google_cloud_auth::credentials::CredentialsProvider for Credentials {
709 async fn headers(
710 &self,
711 extensions: http::Extensions,
712 ) -> std::result::Result<
713 google_cloud_auth::credentials::CacheableResource<http::HeaderMap>,
714 google_cloud_gax::error::CredentialsError,
715 >;
716 async fn universe_domain(&self) -> Option<String>;
717 }
718 }
719
720 async fn http_request_builder(
721 inner: Arc<StorageInner>,
722 builder: ReadObject,
723 ) -> crate::Result<HttpRequestBuilder> {
724 let reader = Reader {
725 inner,
726 request: builder.request,
727 options: builder.options,
728 };
729 reader.http_request_builder().await
730 }
731
732 #[tokio::test]
733 async fn test_clone() {
734 let inner = test_inner_client(test_builder()).await;
735 let stub = crate::storage::transport::Storage::new_test(inner.clone());
736 let options = {
737 let mut o = RequestOptions::new();
738 o.set_resumable_upload_threshold(12345_usize);
739 o
740 };
741 let builder = ReadObject::new(stub, "projects/_/buckets/bucket", "object", options);
742
743 let clone = builder.clone();
744 assert!(Arc::ptr_eq(&clone.stub, &builder.stub));
745 assert_eq!(clone.request, builder.request);
746 assert_eq!(clone.options.resumable_upload_threshold(), 12345_usize);
747 }
748
749 #[tokio::test]
751 async fn test_read_is_send_and_static() -> Result {
752 let client = Storage::builder()
753 .with_credentials(Anonymous::new().build())
754 .build()
755 .await?;
756
757 fn need_send<T: Send>(_val: &T) {}
758 fn need_sync<T: Sync>(_val: &T) {}
759 fn need_static<T: 'static>(_val: &T) {}
760
761 let read = client.read_object("projects/_/buckets/test-bucket", "test-object");
762 need_send(&read);
763 need_sync(&read);
764 need_static(&read);
765
766 let read = client
767 .read_object("projects/_/buckets/test-bucket", "test-object")
768 .send();
769 need_send(&read);
770 need_static(&read);
771
772 Ok(())
773 }
774
775 #[tokio::test]
776 async fn read_object_normal() -> Result {
777 let server = Server::run();
778 server.expect(
779 Expectation::matching(all_of![
780 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
781 request::headers(contains(("accept-encoding", "gzip"))),
782 request::query(url_decoded(contains(("alt", "media")))),
783 ])
784 .respond_with(
785 status_code(200)
786 .body("hello world")
787 .append_header("x-goog-generation", 123456),
788 ),
789 );
790
791 let client = Storage::builder()
792 .with_endpoint(format!("http://{}", server.addr()))
793 .with_credentials(Anonymous::new().build())
794 .build()
795 .await?;
796 let mut reader = client
797 .read_object("projects/_/buckets/test-bucket", "test-object")
798 .send()
799 .await?;
800 let mut got = Vec::new();
801 while let Some(b) = reader.next().await.transpose()? {
802 got.extend_from_slice(&b);
803 }
804 assert_eq!(got, b"hello world");
805
806 Ok(())
807 }
808
809 #[tokio::test]
810 async fn read_object_stream() -> Result {
811 let server = Server::run();
812 server.expect(
813 Expectation::matching(all_of![
814 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
815 request::query(url_decoded(contains(("alt", "media")))),
816 ])
817 .respond_with(
818 status_code(200)
819 .append_header("x-goog-generation", 123456)
820 .body("hello world"),
821 ),
822 );
823
824 let client = Storage::builder()
825 .with_endpoint(format!("http://{}", server.addr()))
826 .with_credentials(Anonymous::new().build())
827 .build()
828 .await?;
829 let response = client
830 .read_object("projects/_/buckets/test-bucket", "test-object")
831 .send()
832 .await?;
833 let result: Vec<_> = response.into_stream().try_collect().await?;
834 assert_eq!(result, vec![bytes::Bytes::from_static(b"hello world")]);
835
836 Ok(())
837 }
838
839 #[tokio::test]
840 async fn read_object_next_then_consume_response() -> Result {
841 const BLOCK_SIZE: usize = 500;
843 let mut contents = Vec::new();
844 for i in 0..50 {
845 contents.extend_from_slice(&[i as u8; BLOCK_SIZE]);
846 }
847
848 let u = crc32c::crc32c(&contents);
850 let value = base64::prelude::BASE64_STANDARD.encode(u.to_be_bytes());
851
852 let server = Server::run();
853 server.expect(
854 Expectation::matching(all_of![
855 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
856 request::query(url_decoded(contains(("alt", "media")))),
857 ])
858 .times(1)
859 .respond_with(
860 status_code(200)
861 .body(contents.clone())
862 .append_header("x-goog-hash", format!("crc32c={value}"))
863 .append_header("x-goog-generation", 123456),
864 ),
865 );
866
867 let client = Storage::builder()
868 .with_endpoint(format!("http://{}", server.addr()))
869 .with_credentials(Anonymous::new().build())
870 .build()
871 .await?;
872
873 let mut response = client
875 .read_object("projects/_/buckets/test-bucket", "test-object")
876 .send()
877 .await?;
878
879 let mut all_bytes = bytes::BytesMut::new();
880 let chunk = response.next().await.transpose()?.unwrap();
881 assert!(!chunk.is_empty());
882 all_bytes.extend(chunk);
883 use futures::StreamExt;
884 let mut stream = response.into_stream();
885 while let Some(chunk) = stream.next().await.transpose()? {
886 all_bytes.extend(chunk);
887 }
888 assert_eq!(all_bytes, contents);
889
890 Ok(())
891 }
892
893 #[tokio::test]
894 async fn read_object_not_found() -> Result {
895 let server = Server::run();
896 server.expect(
897 Expectation::matching(all_of![
898 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
899 request::query(url_decoded(contains(("alt", "media")))),
900 ])
901 .respond_with(status_code(404).body("NOT FOUND")),
902 );
903
904 let client = Storage::builder()
905 .with_endpoint(format!("http://{}", server.addr()))
906 .with_credentials(Anonymous::new().build())
907 .build()
908 .await?;
909 let err = client
910 .read_object("projects/_/buckets/test-bucket", "test-object")
911 .send()
912 .await
913 .expect_err("expected a not found error");
914 assert_eq!(err.http_status_code(), Some(404));
915
916 Ok(())
917 }
918
919 #[tokio::test]
920 async fn read_object_incorrect_crc32c_check() -> Result {
921 let u = crc32c::crc32c("goodbye world".as_bytes());
923 let value = base64::prelude::BASE64_STANDARD.encode(u.to_be_bytes());
924
925 let server = Server::run();
926 server.expect(
927 Expectation::matching(all_of![
928 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
929 request::query(url_decoded(contains(("alt", "media")))),
930 ])
931 .times(3)
932 .respond_with(
933 status_code(200)
934 .body("hello world")
935 .append_header("x-goog-hash", format!("crc32c={value}"))
936 .append_header("x-goog-generation", 123456),
937 ),
938 );
939
940 let client = Storage::builder()
941 .with_endpoint(format!("http://{}", server.addr()))
942 .with_credentials(Anonymous::new().build())
943 .build()
944 .await?;
945 let mut response = client
946 .read_object("projects/_/buckets/test-bucket", "test-object")
947 .send()
948 .await?;
949 let mut partial = Vec::new();
950 let mut err = None;
951 while let Some(r) = response.next().await {
952 match r {
953 Ok(b) => partial.extend_from_slice(&b),
954 Err(e) => err = Some(e),
955 };
956 }
957 assert_eq!(partial, b"hello world");
958 let err = err.expect("expect error on incorrect crc32c");
959 let source = err.source().and_then(|e| e.downcast_ref::<ReadError>());
960 assert!(
961 matches!(
962 source,
963 Some(&ReadError::ChecksumMismatch(
964 ChecksumMismatch::Crc32c { .. }
965 ))
966 ),
967 "err={err:?}"
968 );
969
970 let mut response = client
971 .read_object("projects/_/buckets/test-bucket", "test-object")
972 .send()
973 .await?;
974 let err: crate::Error = async {
975 {
976 while (response.next().await.transpose()?).is_some() {}
977 Ok(())
978 }
979 }
980 .await
981 .expect_err("expect error on incorrect crc32c");
982 let source = err.source().and_then(|e| e.downcast_ref::<ReadError>());
983 assert!(
984 matches!(
985 source,
986 Some(&ReadError::ChecksumMismatch(
987 ChecksumMismatch::Crc32c { .. }
988 ))
989 ),
990 "err={err:?}"
991 );
992
993 use futures::TryStreamExt;
994 let err = client
995 .read_object("projects/_/buckets/test-bucket", "test-object")
996 .send()
997 .await?
998 .into_stream()
999 .try_collect::<Vec<bytes::Bytes>>()
1000 .await
1001 .expect_err("expect error on incorrect crc32c");
1002 let source = err.source().and_then(|e| e.downcast_ref::<ReadError>());
1003 assert!(
1004 matches!(
1005 source,
1006 Some(&ReadError::ChecksumMismatch(
1007 ChecksumMismatch::Crc32c { .. }
1008 ))
1009 ),
1010 "err={err:?}"
1011 );
1012 Ok(())
1013 }
1014
1015 #[tokio::test]
1016 async fn read_object_disable_crc32c() -> Result {
1017 let inner = test_inner_client(test_builder()).await;
1018 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1019
1020 let builder = ReadObject::new(
1021 stub,
1022 "projects/_/buckets/bucket",
1023 "object",
1024 inner.options.clone(),
1025 )
1026 .compute_crc32c(false);
1027
1028 assert!(builder.options.checksum.crc32c.is_none());
1029 Ok(())
1030 }
1031
1032 #[tokio::test]
1033 async fn read_object_enable_crc32c() -> Result {
1034 let inner = test_inner_client(test_builder()).await;
1035 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1036
1037 let builder = ReadObject::new(
1038 stub,
1039 "projects/_/buckets/bucket",
1040 "object",
1041 inner.options.clone(),
1042 )
1043 .compute_crc32c(false) .compute_crc32c(true);
1045
1046 assert!(builder.options.checksum.crc32c.is_some());
1047 Ok(())
1048 }
1049
1050 #[tokio::test]
1051 async fn read_object_disable_crc32c_ignores_mismatch() -> Result {
1052 let server = Server::run();
1053 let u = crc32c::crc32c("goodbye world".as_bytes());
1055 let value = base64::prelude::BASE64_STANDARD.encode(u.to_be_bytes());
1056 server.expect(
1057 Expectation::matching(all_of![
1058 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
1059 request::query(url_decoded(contains(("alt", "media")))),
1060 ])
1061 .respond_with(
1062 status_code(200)
1063 .body("hello world")
1064 .append_header("x-goog-hash", format!("crc32c={value}"))
1065 .append_header("x-goog-generation", 123456),
1066 ),
1067 );
1068
1069 let client = Storage::builder()
1070 .with_endpoint(format!("http://{}", server.addr()))
1071 .with_credentials(Anonymous::new().build())
1072 .build()
1073 .await?;
1074 let mut response = client
1075 .read_object("projects/_/buckets/test-bucket", "test-object")
1076 .compute_crc32c(false)
1077 .send()
1078 .await?;
1079 let mut got = Vec::new();
1080 while let Some(b) = response.next().await.transpose()? {
1081 got.extend_from_slice(&b);
1082 }
1083
1084 assert_eq!(got, b"hello world");
1085 Ok(())
1086 }
1087
1088 #[tokio::test]
1089 async fn read_object_incorrect_md5_check() -> Result {
1090 let digest = md5::compute("goodbye world".as_bytes());
1092 let value = base64::prelude::BASE64_STANDARD.encode(digest.as_ref());
1093
1094 let server = Server::run();
1095 server.expect(
1096 Expectation::matching(all_of![
1097 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
1098 request::query(url_decoded(contains(("alt", "media")))),
1099 ])
1100 .times(1)
1101 .respond_with(
1102 status_code(200)
1103 .body("hello world")
1104 .append_header("x-goog-hash", format!("md5={value}"))
1105 .append_header("x-goog-generation", 123456),
1106 ),
1107 );
1108
1109 let client = Storage::builder()
1110 .with_endpoint(format!("http://{}", server.addr()))
1111 .with_credentials(Anonymous::new().build())
1112 .build()
1113 .await?;
1114 let mut response = client
1115 .read_object("projects/_/buckets/test-bucket", "test-object")
1116 .compute_md5()
1117 .send()
1118 .await?;
1119 let mut partial = Vec::new();
1120 let mut err = None;
1121 while let Some(r) = response.next().await {
1122 match r {
1123 Ok(b) => partial.extend_from_slice(&b),
1124 Err(e) => err = Some(e),
1125 };
1126 }
1127 assert_eq!(partial, b"hello world");
1128 let err = err.expect("expect error on incorrect md5");
1129 let source = err.source().and_then(|e| e.downcast_ref::<ReadError>());
1130 assert!(
1131 matches!(
1132 source,
1133 Some(&ReadError::ChecksumMismatch(ChecksumMismatch::Md5 { .. }))
1134 ),
1135 "err={err:?}"
1136 );
1137
1138 Ok(())
1139 }
1140
1141 #[tokio::test]
1142 async fn read_object_with_user_agent() -> Result {
1143 use http::header::USER_AGENT;
1144
1145 let user_agent = "quick_fox_lazy_dog/1.2.3";
1146 let server = Server::run();
1147 server.expect(
1148 Expectation::matching(all_of![
1149 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
1150 request::headers(contains(("accept-encoding", "gzip"))),
1151 request::headers(contains((USER_AGENT.as_str(), user_agent))),
1152 request::query(url_decoded(contains(("alt", "media")))),
1153 ])
1154 .respond_with(
1155 status_code(200)
1156 .body("hello world")
1157 .append_header("x-goog-generation", 123456),
1158 ),
1159 );
1160
1161 let client = Storage::builder()
1162 .with_endpoint(format!("http://{}", server.addr()))
1163 .with_credentials(Anonymous::new().build())
1164 .build()
1165 .await?;
1166 let mut reader = client
1167 .read_object("projects/_/buckets/test-bucket", "test-object")
1168 .with_user_agent(user_agent)
1169 .send()
1170 .await?;
1171 let mut got = Vec::new();
1172 while let Some(b) = reader.next().await.transpose()? {
1173 got.extend_from_slice(&b);
1174 }
1175 assert_eq!(got, b"hello world");
1176
1177 Ok(())
1178 }
1179
1180 #[tokio::test]
1181 async fn read_object_with_quota_project() -> Result {
1182 const PROJECT_NAME: &str = "project_lazy_dog";
1183 let server = Server::run();
1184 server.expect(
1185 Expectation::matching(all_of![
1186 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
1187 request::headers(contains(("x-goog-user-project", PROJECT_NAME))),
1188 ])
1189 .respond_with(
1190 status_code(200)
1191 .body("hello world")
1192 .append_header("x-goog-generation", 123456),
1193 ),
1194 );
1195
1196 let client = Storage::builder()
1197 .with_endpoint(format!("http://{}", server.addr()))
1198 .with_credentials(Anonymous::new().build())
1199 .build()
1200 .await?;
1201 let mut reader = client
1202 .read_object("projects/_/buckets/test-bucket", "test-object")
1203 .with_quota_project(PROJECT_NAME)
1204 .send()
1205 .await?;
1206 let mut got = Vec::new();
1207 while let Some(b) = reader.next().await.transpose()? {
1208 got.extend_from_slice(&b);
1209 }
1210 assert_eq!(got, b"hello world");
1211
1212 Ok(())
1213 }
1214
1215 #[tokio::test]
1216 async fn read_object_strips_credential_quota_project() -> Result {
1217 const PROJECT_NAME: &str = "project_lazy_dog";
1218 const CRED_QUOTA_PROJECT: &str = "cred_quota_project";
1219 let server = Server::run();
1220 server.expect(
1221 Expectation::matching(all_of![
1222 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
1223 request::headers(contains(("x-goog-user-project", PROJECT_NAME))),
1224 request::headers(not(contains(("x-goog-user-project", CRED_QUOTA_PROJECT)))),
1225 ])
1226 .times(1)
1227 .respond_with(
1228 status_code(200)
1229 .body("hello world")
1230 .append_header("x-goog-generation", 123456),
1231 ),
1232 );
1233
1234 let mut mock = MockCredentials::new();
1235 mock.expect_headers().returning(|_exts: http::Extensions| {
1236 let mut map = http::HeaderMap::new();
1237 map.insert(
1238 http::header::AUTHORIZATION,
1239 http::HeaderValue::from_static("Bearer test-token"),
1240 );
1241 map.insert(
1242 "x-goog-user-project",
1243 http::HeaderValue::from_static(CRED_QUOTA_PROJECT),
1244 );
1245 Ok(CacheableResource::New {
1246 data: map,
1247 entity_tag: EntityTag::default(),
1248 })
1249 });
1250 mock.expect_universe_domain().returning(|| None);
1251
1252 let client = Storage::builder()
1253 .with_endpoint(format!("http://{}", server.addr()))
1254 .with_credentials(Credentials::from(mock))
1255 .build()
1256 .await?;
1257 let mut reader = client
1258 .read_object("projects/_/buckets/test-bucket", "test-object")
1259 .with_quota_project(PROJECT_NAME)
1260 .send()
1261 .await?;
1262 let mut got = Vec::new();
1263 while let Some(b) = reader.next().await.transpose()? {
1264 got.extend_from_slice(&b);
1265 }
1266 assert_eq!(got, b"hello world");
1267
1268 Ok(())
1269 }
1270
1271 #[tokio::test]
1272 async fn read_object_retry_preserves_quota_project() -> Result {
1273 const PROJECT_NAME: &str = "project_lazy_dog";
1274 let server = Server::run();
1275 server.expect(
1276 Expectation::matching(all_of![
1277 request::method_path("GET", "/storage/v1/b/test-bucket/o/test-object"),
1278 request::headers(contains(("x-goog-user-project", PROJECT_NAME))),
1279 ])
1280 .times(2)
1281 .respond_with(cycle![
1282 status_code(503),
1283 status_code(200)
1284 .body("hello")
1285 .append_header("x-goog-generation", 1)
1286 ]),
1287 );
1288
1289 let client = Storage::builder()
1290 .with_endpoint(format!("http://{}", server.addr()))
1291 .with_credentials(Anonymous::new().build())
1292 .build()
1293 .await?;
1294 let mut reader = client
1295 .read_object("projects/_/buckets/test-bucket", "test-object")
1296 .with_quota_project(PROJECT_NAME)
1297 .send()
1298 .await?;
1299 let mut got = Vec::new();
1300 while let Some(b) = reader.next().await.transpose()? {
1301 got.extend_from_slice(&b);
1302 }
1303 assert_eq!(got, b"hello");
1304
1305 Ok(())
1306 }
1307
1308 #[tokio::test]
1309 async fn read_object() -> Result {
1310 let inner = test_inner_client(test_builder()).await;
1311 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1312 let builder = ReadObject::new(
1313 stub,
1314 "projects/_/buckets/bucket",
1315 "object",
1316 inner.options.clone(),
1317 );
1318 let request = http_request_builder(inner, builder)
1319 .await?
1320 .build_for_tests()
1321 .await?;
1322
1323 assert_eq!(request.method(), Method::GET);
1324 assert_eq!(
1325 request.url().as_str(),
1326 "http://private.googleapis.com/storage/v1/b/bucket/o/object?alt=media"
1327 );
1328 Ok(())
1329 }
1330
1331 #[tokio::test]
1332 async fn read_object_error_credentials() -> Result {
1333 let inner =
1334 test_inner_client(test_builder().with_credentials(error_credentials(false))).await;
1335 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1336 let builder = ReadObject::new(
1337 stub,
1338 "projects/_/buckets/bucket",
1339 "object",
1340 inner.options.clone(),
1341 );
1342 let _ = http_request_builder(inner, builder)
1343 .await?
1344 .build_for_tests()
1345 .await
1346 .inspect_err(|e| assert!(e.is_authentication()))
1347 .expect_err("invalid credentials should err");
1348 Ok(())
1349 }
1350
1351 #[tokio::test]
1352 async fn read_object_bad_bucket() -> Result {
1353 let inner = test_inner_client(test_builder()).await;
1354 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1355 let builder = ReadObject::new(stub, "malformed", "object", inner.options.clone());
1356 let _ = http_request_builder(inner, builder)
1357 .await
1358 .expect_err("malformed bucket string should error");
1359 Ok(())
1360 }
1361
1362 #[tokio::test]
1363 async fn read_object_query_params() -> Result {
1364 let inner = test_inner_client(test_builder()).await;
1365 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1366 let builder = ReadObject::new(
1367 stub,
1368 "projects/_/buckets/bucket",
1369 "object",
1370 inner.options.clone(),
1371 )
1372 .set_generation(5)
1373 .set_if_generation_match(10)
1374 .set_if_generation_not_match(20)
1375 .set_if_metageneration_match(30)
1376 .set_if_metageneration_not_match(40);
1377 let request = http_request_builder(inner, builder)
1378 .await?
1379 .build_for_tests()
1380 .await?;
1381
1382 assert_eq!(request.method(), Method::GET);
1383 let want_pairs: HashMap<String, String> = [
1384 ("alt", "media"),
1385 ("generation", "5"),
1386 ("ifGenerationMatch", "10"),
1387 ("ifGenerationNotMatch", "20"),
1388 ("ifMetagenerationMatch", "30"),
1389 ("ifMetagenerationNotMatch", "40"),
1390 ]
1391 .iter()
1392 .map(|(k, v)| (k.to_string(), v.to_string()))
1393 .collect();
1394 let query_pairs: HashMap<String, String> = request
1395 .url()
1396 .query_pairs()
1397 .map(|param| (param.0.to_string(), param.1.to_string()))
1398 .collect();
1399 assert_eq!(query_pairs.len(), want_pairs.len());
1400 assert_eq!(query_pairs, want_pairs);
1401 Ok(())
1402 }
1403
1404 #[tokio::test]
1405 async fn read_object_default_headers() -> Result {
1406 let inner = test_inner_client(test_builder()).await;
1408 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1409 let builder = ReadObject::new(
1410 stub,
1411 "projects/_/buckets/bucket",
1412 "object",
1413 inner.options.clone(),
1414 );
1415 let request = http_request_builder(inner, builder)
1416 .await?
1417 .build_for_tests()
1418 .await?;
1419
1420 assert_eq!(request.method(), Method::GET);
1421 assert_eq!(
1422 request.url().as_str(),
1423 "http://private.googleapis.com/storage/v1/b/bucket/o/object?alt=media"
1424 );
1425
1426 let want = [("accept-encoding", "gzip")];
1427 let headers = request.headers();
1428 for (name, value) in want {
1429 assert_eq!(
1430 headers.get(name).and_then(|h| h.to_str().ok()),
1431 Some(value),
1432 "{request:?}"
1433 );
1434 }
1435 Ok(())
1436 }
1437
1438 #[tokio::test]
1439 async fn read_object_automatic_decompression_headers() -> Result {
1440 let inner = test_inner_client(test_builder()).await;
1442 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1443 let builder = ReadObject::new(
1444 stub,
1445 "projects/_/buckets/bucket",
1446 "object",
1447 inner.options.clone(),
1448 )
1449 .with_automatic_decompression(true);
1450 let request = http_request_builder(inner, builder)
1451 .await?
1452 .build_for_tests()
1453 .await?;
1454
1455 assert_eq!(request.method(), Method::GET);
1456 assert_eq!(
1457 request.url().as_str(),
1458 "http://private.googleapis.com/storage/v1/b/bucket/o/object?alt=media"
1459 );
1460
1461 let headers = request.headers();
1462 assert!(headers.get("accept-encoding").is_none(), "{request:?}");
1463 Ok(())
1464 }
1465
1466 #[tokio::test]
1467 async fn read_object_encryption_headers() -> Result {
1468 let (key, key_base64, _, key_sha256_base64) = create_key_helper();
1470
1471 let inner = test_inner_client(test_builder()).await;
1473 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1474 let builder = ReadObject::new(
1475 stub,
1476 "projects/_/buckets/bucket",
1477 "object",
1478 inner.options.clone(),
1479 )
1480 .set_key(KeyAes256::new(&key)?);
1481 let request = http_request_builder(inner, builder)
1482 .await?
1483 .build_for_tests()
1484 .await?;
1485
1486 assert_eq!(request.method(), Method::GET);
1487 assert_eq!(
1488 request.url().as_str(),
1489 "http://private.googleapis.com/storage/v1/b/bucket/o/object?alt=media"
1490 );
1491
1492 let want = [
1493 ("x-goog-encryption-algorithm", "AES256".to_string()),
1494 ("x-goog-encryption-key", key_base64),
1495 ("x-goog-encryption-key-sha256", key_sha256_base64),
1496 ];
1497
1498 let headers = request.headers();
1499 for (name, value) in want {
1500 assert_eq!(
1501 headers.get(name).and_then(|h| h.to_str().ok()),
1502 Some(value.as_str())
1503 );
1504 }
1505 Ok(())
1506 }
1507
1508 #[test_case(ReadRange::all(), None; "no headers needed")]
1509 #[test_case(ReadRange::offset(10), Some(&http::HeaderValue::from_static("bytes=10-")); "offset only")]
1510 #[test_case(ReadRange::tail(2000), Some(&http::HeaderValue::from_static("bytes=-2000")); "negative offset")]
1511 #[test_case(ReadRange::segment(0, 100), Some(&http::HeaderValue::from_static("bytes=0-99")); "limit only")]
1512 #[test_case(ReadRange::segment(1000, 100), Some(&http::HeaderValue::from_static("bytes=1000-1099")); "offset and limit")]
1513 #[tokio::test]
1514 async fn range_header(input: ReadRange, want: Option<&http::HeaderValue>) -> Result {
1515 let inner = test_inner_client(test_builder()).await;
1516 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1517 let builder = ReadObject::new(
1518 stub,
1519 "projects/_/buckets/bucket",
1520 "object",
1521 inner.options.clone(),
1522 )
1523 .set_read_range(input.clone());
1524 let request = http_request_builder(inner, builder)
1525 .await?
1526 .build_for_tests()
1527 .await?;
1528
1529 assert_eq!(request.method(), Method::GET);
1530 assert_eq!(
1531 request.url().as_str(),
1532 "http://private.googleapis.com/storage/v1/b/bucket/o/object?alt=media"
1533 );
1534
1535 assert_eq!(request.headers().get("range"), want);
1536 Ok(())
1537 }
1538
1539 #[test_case("projects/p", "projects%2Fp")]
1540 #[test_case("kebab-case", "kebab-case")]
1541 #[test_case("dot.name", "dot.name")]
1542 #[test_case("under_score", "under_score")]
1543 #[test_case("tilde~123", "tilde~123")]
1544 #[test_case("exclamation!point!", "exclamation%21point%21")]
1545 #[test_case("spaces spaces", "spaces%20%20%20spaces")]
1546 #[test_case("encode%percent%21", "encode%25percent%2521")]
1547 #[test_case("100%25", "100%2525")]
1548 #[test_case("foo%2Fbar", "foo%252Fbar")]
1549 #[test_case(".%2e", ".%252e")]
1550 #[test_case("%2e%2e", "%252e%252e")]
1551 #[test_case("double\"quote", "double%22quote")]
1552 #[test_case("less<than", "less%3Cthan")]
1553 #[test_case("greater>than", "greater%3Ethan")]
1554 #[test_case("back\\slash", "back%5Cslash")]
1555 #[test_case(
1556 "tenant-a/uploads\\..\\..\\tenant-b/secret",
1557 "tenant-a%2Fuploads%5C..%5C..%5Ctenant-b%2Fsecret"
1558 )]
1559 #[test_case("caret^", "caret%5E")]
1560 #[test_case("back`tick", "back%60tick")]
1561 #[test_case("curly{brace}", "curly%7Bbrace%7D")]
1562 #[test_case("vertical|bar", "vertical%7Cbar")]
1563 #[test_case("test-\u{e9}", "test-%C3%A9")]
1564 #[test_case(
1565 "testall !#$&'()*+,/:;=?@[]",
1566 "testall%20%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"
1567 )]
1568 #[tokio::test]
1569 async fn test_percent_encoding_object_name(name: &str, want: &str) -> Result {
1570 let inner = test_inner_client(test_builder()).await;
1572 let stub = crate::storage::transport::Storage::new_test(inner.clone());
1573 let builder = ReadObject::new(
1574 stub,
1575 "projects/_/buckets/bucket",
1576 name,
1577 inner.options.clone(),
1578 );
1579 let request = http_request_builder(inner, builder)
1581 .await?
1582 .build_for_tests()
1583 .await?;
1584 assert_eq!(
1586 request.url().path(),
1587 format!("/storage/v1/b/bucket/o/{want}")
1588 );
1589 Ok(())
1590 }
1591
1592 #[test_case("x-guploader-response-body-transformations", "gunzipped", true)]
1593 #[test_case("x-guploader-response-body-transformations", "no match", false)]
1594 #[test_case("warning", "214 UploadServer gunzipped", true)]
1595 #[test_case("warning", "no match", false)]
1596 #[test_case("unused", "unused", false)]
1597 fn test_is_gunzipped(name: &'static str, value: &'static str, want: bool) -> Result {
1598 let response = http::Response::builder()
1599 .status(200)
1600 .header(name, value)
1601 .body(Vec::new())?;
1602 let response = Response::from(response);
1603 let got = Reader::is_gunzipped(&response);
1604 assert_eq!(got, want, "{response:?}");
1605 Ok(())
1606 }
1607}