1pub use crate::resources::bucket::Owner;
2use crate::resources::object_access_control::ObjectAccessControl;
3use futures_util::Stream;
4#[cfg(feature = "global-client")]
5use futures_util::TryStream;
6use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
7use std::collections::HashMap;
8
9#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct Object {
13 pub kind: String,
15 pub id: String,
17 pub self_link: String,
19 pub name: String,
21 pub bucket: String,
23 #[serde(deserialize_with = "crate::from_str")]
25 pub generation: i64,
26 #[serde(deserialize_with = "crate::from_str")]
30 pub metageneration: i64,
31 pub content_type: Option<String>,
34 pub time_created: chrono::DateTime<chrono::Utc>,
36 pub updated: chrono::DateTime<chrono::Utc>,
38 pub time_deleted: Option<chrono::DateTime<chrono::Utc>>,
41 pub temporary_hold: Option<bool>,
43 pub event_based_hold: Option<bool>,
45 pub retention_expiration_time: Option<chrono::DateTime<chrono::Utc>>,
48 pub storage_class: String,
50 pub time_storage_class_updated: chrono::DateTime<chrono::Utc>,
53 #[serde(deserialize_with = "crate::from_str")]
55 pub size: u64,
56 pub md5_hash: Option<String>,
59 pub media_link: String,
61 pub content_encoding: Option<String>,
63 pub content_disposition: Option<String>,
65 pub content_language: Option<String>,
67 pub cache_control: Option<String>,
70 pub metadata: Option<std::collections::HashMap<String, String>>,
72 pub acl: Option<Vec<ObjectAccessControl>>,
76 pub owner: Option<Owner>,
80 pub crc32c: String,
84 #[serde(default, deserialize_with = "crate::from_str_opt")]
89 pub component_count: Option<i32>,
90 pub etag: String,
92 pub customer_encryption: Option<CustomerEncrypton>,
94 pub kms_key_name: Option<String>,
96}
97
98#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct CustomerEncrypton {
102 pub encryption_algorithm: String,
104 pub key_sha256: String,
106}
107
108#[derive(Debug, PartialEq, serde::Serialize)]
110#[serde(rename_all = "camelCase")]
111pub struct ComposeRequest {
112 pub kind: String,
114 pub source_objects: Vec<SourceObject>,
116 pub destination: Option<Object>,
118}
119
120#[derive(Debug, PartialEq, serde::Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct SourceObject {
124 pub name: String,
127 pub generation: Option<i64>,
129 pub object_preconditions: Option<ObjectPrecondition>,
131}
132
133#[derive(Debug, PartialEq, serde::Serialize)]
135#[serde(rename_all = "camelCase")]
136pub struct ObjectPrecondition {
137 pub if_generation_match: i64,
141}
142
143#[derive(Debug, PartialEq, serde::Serialize, Default, Clone)]
148#[serde(rename_all = "camelCase")]
149pub struct ListRequest {
150 pub delimiter: Option<String>,
153
154 pub end_offset: Option<String>,
158
159 pub include_trailing_delimiter: Option<bool>,
163
164 pub max_results: Option<usize>,
169
170 pub page_token: Option<String>,
177
178 pub prefix: Option<String>,
180
181 pub projection: Option<Projection>,
183
184 pub start_offset: Option<String>,
188
189 pub versions: Option<bool>,
193}
194
195#[derive(Debug, PartialEq, serde::Serialize, Clone)]
197#[serde(rename_all = "camelCase")]
198pub enum Projection {
199 Full,
201 NoAcl,
203}
204
205#[derive(Debug, serde::Deserialize, Default)]
207#[serde(rename_all = "camelCase")]
208pub struct ObjectList {
209 pub kind: String,
211
212 #[serde(default = "Vec::new")]
214 pub items: Vec<Object>,
215
216 #[serde(default = "Vec::new")]
220 pub prefixes: Vec<String>,
221
222 pub next_page_token: Option<String>,
226}
227
228#[derive(Debug, serde::Deserialize)]
229#[serde(rename_all = "camelCase")]
230#[allow(dead_code)]
231pub(crate) struct RewriteResponse {
232 kind: String,
233 total_bytes_rewritten: String,
234 object_size: String,
235 done: bool,
236 pub(crate) resource: Object,
237}
238
239impl Object {
240 #[cfg(feature = "global-client")]
256 pub async fn create(
257 bucket: &str,
258 file: Vec<u8>,
259 filename: &str,
260 mime_type: &str,
261 ) -> crate::Result<Self> {
262 crate::CLOUD_CLIENT
263 .object()
264 .create(bucket, file, filename, mime_type)
265 .await
266 }
267
268 #[cfg(all(feature = "global-client", feature = "sync"))]
273 pub fn create_sync(
274 bucket: &str,
275 file: Vec<u8>,
276 filename: &str,
277 mime_type: &str,
278 ) -> crate::Result<Self> {
279 crate::runtime()?.block_on(Self::create(bucket, file, filename, mime_type))
280 }
281
282 #[cfg(feature = "global-client")]
300 pub async fn create_streamed<S>(
301 bucket: &str,
302 stream: S,
303 length: impl Into<Option<u64>>,
304 filename: &str,
305 mime_type: &str,
306 ) -> crate::Result<Self>
307 where
308 S: TryStream + Send + Sync + 'static,
309 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
310 bytes::Bytes: From<S::Ok>,
311 {
312 crate::CLOUD_CLIENT
313 .object()
314 .create_streamed(bucket, stream, length, filename, mime_type)
315 .await
316 }
317
318 #[cfg(all(feature = "global-client", feature = "sync"))]
323 pub fn create_streamed_sync<R: std::io::Read + Send + 'static>(
324 bucket: &str,
325 mut file: R,
326 length: impl Into<Option<u64>>,
327 filename: &str,
328 mime_type: &str,
329 ) -> crate::Result<Self> {
330 let mut buffer = Vec::new();
331 file.read_to_end(&mut buffer)
332 .map_err(|e| crate::Error::Other(e.to_string()))?;
333
334 let stream = futures_util::stream::once(async { Ok::<_, crate::Error>(buffer) });
335
336 crate::runtime()?.block_on(Self::create_streamed(
337 bucket, stream, length, filename, mime_type,
338 ))
339 }
340
341 #[cfg(feature = "global-client")]
356 pub async fn list(
357 bucket: &str,
358 list_request: ListRequest,
359 ) -> crate::Result<impl Stream<Item = crate::Result<ObjectList>> + '_> {
360 crate::CLOUD_CLIENT
361 .object()
362 .list(bucket, list_request)
363 .await
364 }
365
366 #[cfg(all(feature = "global-client", feature = "sync"))]
371 pub fn list_sync(bucket: &str, list_request: ListRequest) -> crate::Result<Vec<ObjectList>> {
372 use futures_util::TryStreamExt;
373
374 let rt = crate::runtime()?;
375 let listed = rt.block_on(Self::list(bucket, list_request))?;
376 rt.block_on(listed.try_collect())
377 }
378
379 #[cfg(feature = "global-client")]
391 pub async fn read(bucket: &str, file_name: &str) -> crate::Result<Self> {
392 crate::CLOUD_CLIENT.object().read(bucket, file_name).await
393 }
394
395 #[cfg(all(feature = "global-client", feature = "sync"))]
400 pub fn read_sync(bucket: &str, file_name: &str) -> crate::Result<Self> {
401 crate::runtime()?.block_on(Self::read(bucket, file_name))
402 }
403
404 #[cfg(feature = "global-client")]
416 pub async fn download(bucket: &str, file_name: &str) -> crate::Result<Vec<u8>> {
417 crate::CLOUD_CLIENT
418 .object()
419 .download(bucket, file_name)
420 .await
421 }
422
423 #[cfg(all(feature = "global-client", feature = "sync"))]
428 pub fn download_sync(bucket: &str, file_name: &str) -> crate::Result<Vec<u8>> {
429 crate::runtime()?.block_on(Self::download(bucket, file_name))
430 }
431
432 #[cfg(feature = "global-client")]
452 pub async fn download_streamed(
453 bucket: &str,
454 file_name: &str,
455 ) -> crate::Result<impl Stream<Item = crate::Result<u8>> + Unpin> {
456 crate::CLOUD_CLIENT
457 .object()
458 .download_streamed(bucket, file_name)
459 .await
460 }
461
462 #[cfg(feature = "global-client")]
476 pub async fn update(&self) -> crate::Result<Self> {
477 crate::CLOUD_CLIENT.object().update(self).await
478 }
479
480 #[cfg(all(feature = "global-client", feature = "sync"))]
485 pub fn update_sync(&self) -> crate::Result<Self> {
486 crate::runtime()?.block_on(self.update())
487 }
488
489 #[cfg(feature = "global-client")]
501 pub async fn delete(bucket: &str, file_name: &str) -> crate::Result<()> {
502 crate::CLOUD_CLIENT.object().delete(bucket, file_name).await
503 }
504
505 #[cfg(all(feature = "global-client", feature = "sync"))]
510 pub fn delete_sync(bucket: &str, file_name: &str) -> crate::Result<()> {
511 crate::runtime()?.block_on(Self::delete(bucket, file_name))
512 }
513
514 #[cfg(feature = "global-client")]
545 pub async fn compose(
546 bucket: &str,
547 req: &ComposeRequest,
548 destination_object: &str,
549 ) -> crate::Result<Self> {
550 crate::CLOUD_CLIENT
551 .object()
552 .compose(bucket, req, destination_object)
553 .await
554 }
555
556 #[cfg(all(feature = "global-client", feature = "sync"))]
561 pub fn compose_sync(
562 bucket: &str,
563 req: &ComposeRequest,
564 destination_object: &str,
565 ) -> crate::Result<Self> {
566 crate::runtime()?.block_on(Self::compose(bucket, req, destination_object))
567 }
568
569 #[cfg(feature = "global-client")]
583 pub async fn copy(&self, destination_bucket: &str, path: &str) -> crate::Result<Self> {
584 crate::CLOUD_CLIENT
585 .object()
586 .copy(self, destination_bucket, path)
587 .await
588 }
589
590 #[cfg(all(feature = "global-client", feature = "sync"))]
595 pub fn copy_sync(&self, destination_bucket: &str, path: &str) -> crate::Result<Self> {
596 crate::runtime()?.block_on(self.copy(destination_bucket, path))
597 }
598
599 #[cfg(feature = "global-client")]
620 pub async fn rewrite(&self, destination_bucket: &str, path: &str) -> crate::Result<Self> {
621 crate::CLOUD_CLIENT
622 .object()
623 .rewrite(self, destination_bucket, path)
624 .await
625 }
626
627 #[cfg(all(feature = "global-client", feature = "sync"))]
632 pub fn rewrite_sync(&self, destination_bucket: &str, path: &str) -> crate::Result<Self> {
633 crate::runtime()?.block_on(self.rewrite(destination_bucket, path))
634 }
635
636 pub fn download_url(&self, duration: u32) -> crate::Result<String> {
654 self.sign(&self.name, duration, "GET", None, &HashMap::new())
655 }
656
657 pub fn download_url_with(
675 &self,
676 duration: u32,
677 opts: crate::DownloadOptions,
678 ) -> crate::Result<String> {
679 self.sign(
680 &self.name,
681 duration,
682 "GET",
683 opts.content_disposition,
684 &HashMap::new(),
685 )
686 }
687
688 pub fn upload_url(&self, duration: u32) -> crate::Result<String> {
706 self.sign(&self.name, duration, "PUT", None, &HashMap::new())
707 }
708
709 pub fn upload_url_with(
730 &self,
731 duration: u32,
732 custom_metadata: HashMap<String, String>,
733 ) -> crate::Result<(String, HashMap<String, String>)> {
734 let url = self.sign(&self.name, duration, "PUT", None, &custom_metadata)?;
735 let mut headers = HashMap::new();
736 for (k, v) in custom_metadata.iter() {
737 headers.insert(format!("x-goog-meta-{}", k.to_string()), v.to_string());
738 }
739 Ok((url, headers))
740 }
741
742 #[inline(always)]
750 fn sign(
751 &self,
752 file_path: &str,
753 duration: u32,
754 http_verb: &str,
755 content_disposition: Option<String>,
756 custom_metadata: &HashMap<String, String>,
757 ) -> crate::Result<String> {
758 if duration > 604800 {
759 let msg = format!(
760 "duration may not be greater than 604800, but was {}",
761 duration
762 );
763 return Err(crate::Error::Other(msg));
764 }
765
766 let mut headers = vec![("host".to_string(), "storage.googleapis.com".to_string())];
768 for (k, v) in custom_metadata.iter() {
770 headers.push((format!("x-goog-meta-{}", k.to_string()), v.to_string()));
771 }
772 headers.sort_unstable_by(|(k1, _), (k2, _)| k1.cmp(k2));
773 let canonical_headers: String = headers
774 .iter()
775 .map(|(k, v)| format!("{}:{}", k.to_lowercase(), v.to_lowercase()))
776 .collect::<Vec<String>>()
777 .join("\n");
778 let signed_headers = headers
779 .iter()
780 .map(|(k, _)| k.to_lowercase())
781 .collect::<Vec<String>>()
782 .join(";");
783
784 let issue_date = chrono::Utc::now();
786 let file_path = self.path_to_resource(file_path);
787 let query_string = Self::get_canonical_query_string(
788 &issue_date,
789 duration,
790 &signed_headers,
791 content_disposition,
792 );
793 let canonical_request = self.get_canonical_request(
794 &file_path,
795 &query_string,
796 http_verb,
797 &canonical_headers,
798 &signed_headers,
799 );
800
801 let hex_hash = hex::encode(crypto::sha256(canonical_request.as_bytes()).as_ref());
803
804 let string_to_sign = format!(
806 "{signing_algorithm}\n\
807 {current_datetime}\n\
808 {credential_scope}\n\
809 {hashed_canonical_request}",
810 signing_algorithm = "GOOG4-RSA-SHA256",
811 current_datetime = issue_date.format("%Y%m%dT%H%M%SZ"),
812 credential_scope = Self::get_credential_scope(&issue_date),
813 hashed_canonical_request = hex_hash,
814 );
815
816 let signature = hex::encode(crypto::rsa_pkcs1_sha256(&string_to_sign)?);
818
819 Ok(format!(
821 "https://storage.googleapis.com{path_to_resource}?\
822 {query_string}&\
823 X-Goog-Signature={request_signature}",
824 path_to_resource = file_path,
825 query_string = query_string,
826 request_signature = signature,
827 ))
828 }
829
830 #[inline(always)]
831 fn get_canonical_request(
832 &self,
833 path: &str,
834 query_string: &str,
835 http_verb: &str,
836 headers: &str,
837 signed_headers: &str,
838 ) -> String {
839 format!(
840 "{http_verb}\n\
841 {path_to_resource}\n\
842 {canonical_query_string}\n\
843 {canonical_headers}\n\
844 \n\
845 {signed_headers}\n\
846 {payload}",
847 http_verb = http_verb,
848 path_to_resource = path,
849 canonical_query_string = query_string,
850 canonical_headers = headers,
851 signed_headers = signed_headers,
852 payload = "UNSIGNED-PAYLOAD",
853 )
854 }
855
856 #[inline(always)]
857 fn get_canonical_query_string(
858 date: &chrono::DateTime<chrono::Utc>,
859 exp: u32,
860 headers: &str,
861 content_disposition: Option<String>,
862 ) -> String {
863 let credential = format!(
864 "{authorizer}/{scope}",
865 authorizer = crate::SERVICE_ACCOUNT.client_email,
866 scope = Self::get_credential_scope(date),
867 );
868 let mut s = format!(
869 "X-Goog-Algorithm={algo}&\
870 X-Goog-Credential={cred}&\
871 X-Goog-Date={date}&\
872 X-Goog-Expires={exp}&\
873 X-Goog-SignedHeaders={signed}",
874 algo = "GOOG4-RSA-SHA256",
875 cred = percent_encode(&credential),
876 date = date.format("%Y%m%dT%H%M%SZ"),
877 exp = exp,
878 signed = percent_encode(headers),
879 );
880 if let Some(cd) = content_disposition {
881 s.push_str(&format!("&response-content-disposition={}", cd));
882 }
883 s
884 }
885
886 #[inline(always)]
887 fn path_to_resource(&self, path: &str) -> String {
888 format!(
889 "/{bucket}/{file_path}",
890 bucket = self.bucket,
891 file_path = percent_encode_noslash(path),
892 )
893 }
894
895 #[inline(always)]
896 fn get_credential_scope(date: &chrono::DateTime<chrono::Utc>) -> String {
897 format!("{}/henk/storage/goog4_request", date.format("%Y%m%d"))
898 }
899}
900
901#[cfg(feature = "openssl")]
902mod openssl {
903 #[inline(always)]
904 pub fn rsa_pkcs1_sha256(message: &str) -> crate::Result<Vec<u8>> {
905 use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer};
906
907 let key = PKey::private_key_from_pem(crate::SERVICE_ACCOUNT.private_key.as_bytes())?;
908 let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
909 signer.update(message.as_bytes())?;
910 Ok(signer.sign_to_vec()?)
911 }
912
913 #[inline(always)]
914 pub fn sha256(bytes: &[u8]) -> impl AsRef<[u8]> {
915 openssl::sha::sha256(bytes)
916 }
917}
918
919#[cfg(feature = "ring")]
920mod ring {
921 #[cfg_attr(all(feature = "ring", feature = "openssl"), allow(dead_code))]
922 #[inline(always)]
923 pub fn rsa_pkcs1_sha256(message: &str) -> crate::Result<Vec<u8>> {
924 use ring::{
925 rand::SystemRandom,
926 signature::{RsaKeyPair, RSA_PKCS1_SHA256},
927 };
928
929 let key_pem = pem::parse(crate::SERVICE_ACCOUNT.private_key.as_bytes())?;
930 let key = RsaKeyPair::from_pkcs8(&key_pem.contents)?;
931 let rng = SystemRandom::new();
932 let mut signature = vec![0; key.public_modulus_len()];
933 key.sign(&RSA_PKCS1_SHA256, &rng, message.as_bytes(), &mut signature)?;
934 Ok(signature)
935 }
936
937 #[cfg_attr(all(feature = "ring", feature = "openssl"), allow(dead_code))]
938 #[inline(always)]
939 pub fn sha256(bytes: &[u8]) -> impl AsRef<[u8]> {
940 use ring::digest::{digest, SHA256};
941 digest(&SHA256, bytes)
942 }
943}
944
945mod crypto {
946 #[cfg(feature = "openssl")]
947 pub use super::openssl::*;
948 #[cfg(all(feature = "ring", not(feature = "openssl")))]
949 pub use super::ring::*;
950}
951
952const ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
953 .remove(b'*')
954 .remove(b'-')
955 .remove(b'.')
956 .remove(b'_');
957
958const NOSLASH_ENCODE_SET: &AsciiSet = &ENCODE_SET.remove(b'/').remove(b'~');
959
960fn percent_encode_noslash(input: &str) -> String {
963 utf8_percent_encode(input, NOSLASH_ENCODE_SET).to_string()
964}
965
966pub(crate) fn percent_encode(input: &str) -> String {
967 utf8_percent_encode(input, ENCODE_SET).to_string()
968}
969
970#[cfg(all(test, feature = "global-client"))]
971mod tests {
972 use super::*;
973 use crate::Error;
974 use futures_util::{stream, StreamExt, TryStreamExt};
975
976 #[tokio::test]
977 async fn create() -> Result<(), Box<dyn std::error::Error>> {
978 let bucket = crate::read_test_bucket().await;
979 Object::create(&bucket.name, vec![0, 1], "test-create", "text/plain").await?;
980 Ok(())
981 }
982
983 #[tokio::test]
984 async fn create_streamed() -> Result<(), Box<dyn std::error::Error>> {
985 let bucket = crate::read_test_bucket().await;
986 let stream = stream::iter([0u8, 1].iter())
987 .map(Ok::<_, Box<dyn std::error::Error + Send + Sync>>)
988 .map_ok(|&b| bytes::BytesMut::from(&[b][..]));
989 Object::create_streamed(
990 &bucket.name,
991 stream,
992 2,
993 "test-create-streamed",
994 "text/plain",
995 )
996 .await?;
997 Ok(())
998 }
999
1000 #[tokio::test]
1001 async fn list() -> Result<(), Box<dyn std::error::Error>> {
1002 let test_bucket = crate::read_test_bucket().await;
1003 let _v: Vec<ObjectList> = Object::list(&test_bucket.name, ListRequest::default())
1004 .await?
1005 .try_collect()
1006 .await?;
1007 Ok(())
1008 }
1009
1010 async fn flattened_list_prefix_stream(
1011 bucket: &str,
1012 prefix: &str,
1013 ) -> Result<Vec<Object>, Box<dyn std::error::Error>> {
1014 let request = ListRequest {
1015 prefix: Some(prefix.into()),
1016 ..Default::default()
1017 };
1018
1019 Ok(Object::list(bucket, request)
1020 .await?
1021 .map_ok(|object_list| object_list.items)
1022 .try_concat()
1023 .await?)
1024 }
1025
1026 #[tokio::test]
1027 async fn list_prefix() -> Result<(), Box<dyn std::error::Error>> {
1028 let test_bucket = crate::read_test_bucket().await;
1029
1030 let prefix_names = [
1031 "test-list-prefix/1",
1032 "test-list-prefix/2",
1033 "test-list-prefix/sub/1",
1034 "test-list-prefix/sub/2",
1035 ];
1036
1037 for name in &prefix_names {
1038 Object::create(&test_bucket.name, vec![0, 1], name, "text/plain").await?;
1039 }
1040
1041 let list = flattened_list_prefix_stream(&test_bucket.name, "test-list-prefix/").await?;
1042 assert_eq!(list.len(), 4);
1043 let list = flattened_list_prefix_stream(&test_bucket.name, "test-list-prefix/sub").await?;
1044 assert_eq!(list.len(), 2);
1045 Ok(())
1046 }
1047
1048 #[tokio::test]
1049 async fn read() -> Result<(), Box<dyn std::error::Error>> {
1050 let bucket = crate::read_test_bucket().await;
1051 Object::create(&bucket.name, vec![0, 1], "test-read", "text/plain").await?;
1052 Object::read(&bucket.name, "test-read").await?;
1053 Ok(())
1054 }
1055
1056 #[tokio::test]
1057 async fn download() -> Result<(), Box<dyn std::error::Error>> {
1058 let bucket = crate::read_test_bucket().await;
1059 let content = b"hello world";
1060 Object::create(
1061 &bucket.name,
1062 content.to_vec(),
1063 "test-download",
1064 "application/octet-stream",
1065 )
1066 .await?;
1067
1068 let data = Object::download(&bucket.name, "test-download").await?;
1069 assert_eq!(data, content);
1070
1071 Ok(())
1072 }
1073
1074 #[tokio::test]
1075 async fn download_streamed() -> Result<(), Box<dyn std::error::Error>> {
1076 let bucket = crate::read_test_bucket().await;
1077 let content = b"hello world";
1078 Object::create(
1079 &bucket.name,
1080 content.to_vec(),
1081 "test-download",
1082 "application/octet-stream",
1083 )
1084 .await?;
1085
1086 let result = Object::download_streamed(&bucket.name, "test-download").await?;
1087 let data = result.try_collect::<Vec<_>>().await?;
1088 assert_eq!(data, content);
1089
1090 Ok(())
1091 }
1092
1093 #[tokio::test]
1094 async fn download_streamed_large() -> Result<(), Box<dyn std::error::Error>> {
1095 let bucket = crate::read_test_bucket().await;
1096 let content = vec![5u8; 1_000_000];
1097 Object::create(
1098 &bucket.name,
1099 content.to_vec(),
1100 "test-download-large",
1101 "application/octet-stream",
1102 )
1103 .await?;
1104
1105 let mut result = Object::download_streamed(&bucket.name, "test-download-large").await?;
1106 let mut data: Vec<u8> = Vec::new();
1107 while let Some(part) = result.next().await {
1108 data.push(part?);
1109 }
1110 assert_eq!(data, content);
1111
1112 Ok(())
1113 }
1114
1115 #[tokio::test]
1116 async fn update() -> Result<(), Box<dyn std::error::Error>> {
1117 let bucket = crate::read_test_bucket().await;
1118 let mut obj = Object::create(&bucket.name, vec![0, 1], "test-update", "text/plain").await?;
1119 obj.content_type = Some("application/xml".to_string());
1120 obj.update().await?;
1121 Ok(())
1122 }
1123
1124 #[tokio::test]
1125 async fn delete() -> Result<(), Box<dyn std::error::Error>> {
1126 let bucket = crate::read_test_bucket().await;
1127 Object::create(&bucket.name, vec![0, 1], "test-delete", "text/plain").await?;
1128
1129 Object::delete(&bucket.name, "test-delete").await?;
1130
1131 let list: Vec<_> = flattened_list_prefix_stream(&bucket.name, "test-delete").await?;
1132 assert!(list.is_empty());
1133
1134 Ok(())
1135 }
1136
1137 #[tokio::test]
1138 async fn delete_nonexistent() -> Result<(), Box<dyn std::error::Error>> {
1139 let bucket = crate::read_test_bucket().await;
1140
1141 let nonexistent_object = "test-delete-nonexistent";
1142
1143 let delete_result = Object::delete(&bucket.name, nonexistent_object).await;
1144
1145 if let Err(Error::Google(google_error_response)) = delete_result {
1146 assert!(google_error_response.to_string().contains(&format!(
1147 "No such object: {}/{}",
1148 bucket.name, nonexistent_object
1149 )));
1150 } else {
1151 panic!("Expected a Google error, instead got {:?}", delete_result);
1152 }
1153
1154 Ok(())
1155 }
1156
1157 #[tokio::test]
1158 async fn compose() -> Result<(), Box<dyn std::error::Error>> {
1159 let bucket = crate::read_test_bucket().await;
1160 let obj1 = Object::create(&bucket.name, vec![0, 1], "test-compose-1", "text/plain").await?;
1161 let obj2 = Object::create(&bucket.name, vec![2, 3], "test-compose-2", "text/plain").await?;
1162 let compose_request = ComposeRequest {
1163 kind: "storage#composeRequest".to_string(),
1164 source_objects: vec![
1165 SourceObject {
1166 name: obj1.name.clone(),
1167 generation: None,
1168 object_preconditions: None,
1169 },
1170 SourceObject {
1171 name: obj2.name.clone(),
1172 generation: None,
1173 object_preconditions: None,
1174 },
1175 ],
1176 destination: None,
1177 };
1178 let obj3 = Object::compose(&bucket.name, &compose_request, "test-concatted-file").await?;
1179 let url = obj3.download_url(100)?;
1180 let content = reqwest::get(&url).await?.text().await?;
1181 assert_eq!(content.as_bytes(), &[0, 1, 2, 3]);
1182 Ok(())
1183 }
1184
1185 #[tokio::test]
1186 async fn copy() -> Result<(), Box<dyn std::error::Error>> {
1187 let bucket = crate::read_test_bucket().await;
1188 let original = Object::create(&bucket.name, vec![2, 3], "test-copy", "text/plain").await?;
1189 original.copy(&bucket.name, "test-copy - copy").await?;
1190 Ok(())
1191 }
1192
1193 #[tokio::test]
1194 async fn rewrite() -> Result<(), Box<dyn std::error::Error>> {
1195 let bucket = crate::read_test_bucket().await;
1196 let obj = Object::create(&bucket.name, vec![0, 1], "test-rewrite", "text/plain").await?;
1197 let obj = obj.rewrite(&bucket.name, "test-rewritten").await?;
1198 let url = obj.download_url(100)?;
1199 let client = reqwest::Client::default();
1200 let download = client.head(&url).send().await?;
1201 assert_eq!(download.status().as_u16(), 200);
1202 Ok(())
1203 }
1204
1205 #[tokio::test]
1206 async fn test_url_encoding() -> Result<(), Box<dyn std::error::Error>> {
1207 let bucket = crate::read_test_bucket().await;
1208 let complicated_names = [
1209 "asdf",
1210 "asdf+1",
1211 "asdf&&+1?=3,,-_()*&^%$#@!`~{}[]\\|:;\"'<>,.?/äöüëß",
1212 "https://www.google.com",
1213 "परिक्षण फाईल",
1214 "测试很重要",
1215 ];
1216 for name in &complicated_names {
1217 let _obj = Object::create(&bucket.name, vec![0, 1], name, "text/plain").await?;
1218 let obj = Object::read(&bucket.name, &name).await.unwrap();
1219 let url = obj.download_url(100)?;
1220 let client = reqwest::Client::default();
1221 let download = client.head(&url).send().await?;
1222 assert_eq!(download.status().as_u16(), 200);
1223 }
1224 Ok(())
1225 }
1226
1227 #[tokio::test]
1228 async fn test_download_url_with() -> Result<(), Box<dyn std::error::Error>> {
1229 let bucket = crate::read_test_bucket().await;
1230 let client = reqwest::Client::new();
1231 let obj = Object::create(&bucket.name, vec![0, 1], "test-rewrite", "text/plain").await?;
1232
1233 let opts1 = crate::DownloadOptions::new().content_disposition("attachment");
1234 let download_url1 = obj.download_url_with(100, opts1)?;
1235 let download1 = client.head(&download_url1).send().await?;
1236 assert_eq!(download1.headers()["content-disposition"], "attachment");
1237 Ok(())
1238 }
1239
1240 #[tokio::test]
1241 async fn test_upload_url() -> Result<(), Box<dyn std::error::Error>> {
1242 let bucket = crate::read_test_bucket().await;
1243 let client = reqwest::Client::new();
1244 let blob_name = "test-upload-url";
1245 let obj = Object::create(&bucket.name, vec![0, 1], blob_name, "text/plain").await?;
1246
1247 let url = obj.upload_url(100).unwrap();
1248 let updated_content = vec![2, 3];
1249 let response = client
1250 .put(&url)
1251 .body(updated_content.clone())
1252 .send()
1253 .await?;
1254 assert!(response.status().is_success());
1255 let data = Object::download(&bucket.name, blob_name).await?;
1256 assert_eq!(data, updated_content);
1257 Ok(())
1258 }
1259
1260 #[tokio::test]
1261 async fn test_upload_url_with() -> Result<(), Box<dyn std::error::Error>> {
1262 let bucket = crate::read_test_bucket().await;
1263 let client = reqwest::Client::new();
1264 let blob_name = "test-upload-url";
1265 let obj = Object::create(&bucket.name, vec![0, 1], blob_name, "text/plain").await?;
1266 let mut custom_metadata = HashMap::new();
1267 custom_metadata.insert(String::from("field"), String::from("value"));
1268
1269 let (url, headers) = obj.upload_url_with(100, custom_metadata).unwrap();
1270 let updated_content = vec![2, 3];
1271 let mut request = client.put(&url).body(updated_content);
1272 for (metadata_field, metadata_value) in headers.iter() {
1273 request = request.header(metadata_field, metadata_value);
1274 }
1275 let response = request.send().await?;
1276 assert!(response.status().is_success());
1277 let updated_obj = Object::read(&bucket.name, blob_name).await?;
1278 let obj_metadata = updated_obj.metadata.unwrap();
1279 assert_eq!(obj_metadata.get("field").unwrap(), "value");
1280 Ok(())
1281 }
1282
1283 #[cfg(all(feature = "openssl", feature = "ring"))]
1284 #[test]
1285 fn check_matching_crypto() {
1286 assert_eq!(
1287 openssl::sha256(b"hello").as_ref(),
1288 ring::sha256(b"hello").as_ref()
1289 );
1290
1291 assert_eq!(
1292 openssl::rsa_pkcs1_sha256("world").unwrap(),
1293 ring::rsa_pkcs1_sha256("world").unwrap(),
1294 );
1295 }
1296
1297 #[cfg(feature = "sync")]
1298 mod sync {
1299 use super::*;
1300
1301 #[test]
1302 fn create() -> Result<(), Box<dyn std::error::Error>> {
1303 let bucket = crate::read_test_bucket_sync();
1304 Object::create_sync(&bucket.name, vec![0, 1], "test-create", "text/plain")?;
1305 Ok(())
1306 }
1307
1308 #[test]
1309 fn create_streamed() -> Result<(), Box<dyn std::error::Error>> {
1310 let bucket = crate::read_test_bucket_sync();
1311 let cursor = std::io::Cursor::new([0, 1]);
1312 Object::create_streamed_sync(
1313 &bucket.name,
1314 cursor,
1315 2,
1316 "test-create-streamed",
1317 "text/plain",
1318 )?;
1319 Ok(())
1320 }
1321
1322 #[test]
1323 fn list() -> Result<(), Box<dyn std::error::Error>> {
1324 let test_bucket = crate::read_test_bucket_sync();
1325 Object::list_sync(&test_bucket.name, ListRequest::default())?;
1326 Ok(())
1327 }
1328
1329 #[test]
1330 fn list_prefix() -> Result<(), Box<dyn std::error::Error>> {
1331 let test_bucket = crate::read_test_bucket_sync();
1332
1333 let prefix_names = [
1334 "test-list-prefix/1",
1335 "test-list-prefix/2",
1336 "test-list-prefix/sub/1",
1337 "test-list-prefix/sub/2",
1338 ];
1339
1340 for name in &prefix_names {
1341 Object::create_sync(&test_bucket.name, vec![0, 1], name, "text/plain")?;
1342 }
1343
1344 let request = ListRequest {
1345 prefix: Some("test-list-prefix/".into()),
1346 ..Default::default()
1347 };
1348 let list = Object::list_sync(&test_bucket.name, request)?;
1349 assert_eq!(list[0].items.len(), 4);
1350
1351 let request = ListRequest {
1352 prefix: Some("test-list-prefix/sub".into()),
1353 ..Default::default()
1354 };
1355 let list = Object::list_sync(&test_bucket.name, request)?;
1356 assert_eq!(list[0].items.len(), 2);
1357 Ok(())
1358 }
1359
1360 #[test]
1361 fn list_prefix_delimiter() -> Result<(), Box<dyn std::error::Error>> {
1362 let test_bucket = crate::read_test_bucket_sync();
1363
1364 let prefix_names = [
1365 "test-list-prefix/1",
1366 "test-list-prefix/2",
1367 "test-list-prefix/sub/1",
1368 "test-list-prefix/sub/2",
1369 ];
1370
1371 for name in &prefix_names {
1372 Object::create_sync(&test_bucket.name, vec![0, 1], name, "text/plain")?;
1373 }
1374
1375 let request = ListRequest {
1376 prefix: Some("test-list-prefix/".into()),
1377 delimiter: Some("/".into()),
1378 ..Default::default()
1379 };
1380 let list = Object::list_sync(&test_bucket.name, request)?;
1381 assert_eq!(list[0].items.len(), 2);
1382 assert_eq!(list[0].prefixes.len(), 1);
1383 Ok(())
1384 }
1385
1386 #[test]
1387 fn read() -> Result<(), Box<dyn std::error::Error>> {
1388 let bucket = crate::read_test_bucket_sync();
1389 Object::create_sync(&bucket.name, vec![0, 1], "test-read", "text/plain")?;
1390 Object::read_sync(&bucket.name, "test-read")?;
1391 Ok(())
1392 }
1393
1394 #[test]
1395 fn download() -> Result<(), Box<dyn std::error::Error>> {
1396 let bucket = crate::read_test_bucket_sync();
1397 let content = b"hello world";
1398 Object::create_sync(
1399 &bucket.name,
1400 content.to_vec(),
1401 "test-download",
1402 "application/octet-stream",
1403 )?;
1404
1405 let data = Object::download_sync(&bucket.name, "test-download")?;
1406 assert_eq!(data, content);
1407
1408 Ok(())
1409 }
1410
1411 #[test]
1412 fn update() -> Result<(), Box<dyn std::error::Error>> {
1413 let bucket = crate::read_test_bucket_sync();
1414 let mut obj =
1415 Object::create_sync(&bucket.name, vec![0, 1], "test-update", "text/plain")?;
1416 obj.content_type = Some("application/xml".to_string());
1417 obj.update_sync()?;
1418 Ok(())
1419 }
1420
1421 #[test]
1422 fn delete() -> Result<(), Box<dyn std::error::Error>> {
1423 let bucket = crate::read_test_bucket_sync();
1424 Object::create_sync(&bucket.name, vec![0, 1], "test-delete", "text/plain")?;
1425
1426 Object::delete_sync(&bucket.name, "test-delete")?;
1427
1428 let request = ListRequest {
1429 prefix: Some("test-delete".into()),
1430 ..Default::default()
1431 };
1432
1433 let list = Object::list_sync(&bucket.name, request)?;
1434 assert!(list[0].items.is_empty());
1435
1436 Ok(())
1437 }
1438
1439 #[test]
1440 fn delete_nonexistent() -> Result<(), Box<dyn std::error::Error>> {
1441 let bucket = crate::read_test_bucket_sync();
1442
1443 let nonexistent_object = "test-delete-nonexistent";
1444
1445 let delete_result = Object::delete_sync(&bucket.name, nonexistent_object);
1446
1447 if let Err(Error::Google(google_error_response)) = delete_result {
1448 assert!(google_error_response.to_string().contains(&format!(
1449 "No such object: {}/{}",
1450 bucket.name, nonexistent_object
1451 )));
1452 } else {
1453 panic!("Expected a Google error, instead got {:?}", delete_result);
1454 }
1455
1456 Ok(())
1457 }
1458
1459 #[test]
1460 fn compose() -> Result<(), Box<dyn std::error::Error>> {
1461 let bucket = crate::read_test_bucket_sync();
1462 let obj1 =
1463 Object::create_sync(&bucket.name, vec![0, 1], "test-compose-1", "text/plain")?;
1464 let obj2 =
1465 Object::create_sync(&bucket.name, vec![2, 3], "test-compose-2", "text/plain")?;
1466 let compose_request = ComposeRequest {
1467 kind: "storage#composeRequest".to_string(),
1468 source_objects: vec![
1469 SourceObject {
1470 name: obj1.name.clone(),
1471 generation: None,
1472 object_preconditions: None,
1473 },
1474 SourceObject {
1475 name: obj2.name.clone(),
1476 generation: None,
1477 object_preconditions: None,
1478 },
1479 ],
1480 destination: None,
1481 };
1482 let obj3 = Object::compose_sync(&bucket.name, &compose_request, "test-concatted-file")?;
1483 let url = obj3.download_url(100)?;
1484 let content = reqwest::blocking::get(&url)?.text()?;
1485 assert_eq!(content.as_bytes(), &[0, 1, 2, 3]);
1486 Ok(())
1487 }
1488
1489 #[test]
1490 fn copy() -> Result<(), Box<dyn std::error::Error>> {
1491 let bucket = crate::read_test_bucket_sync();
1492 let original =
1493 Object::create_sync(&bucket.name, vec![2, 3], "test-copy", "text/plain")?;
1494 original.copy_sync(&bucket.name, "test-copy - copy")?;
1495 Ok(())
1496 }
1497
1498 #[test]
1499 fn rewrite() -> Result<(), Box<dyn std::error::Error>> {
1500 let bucket = crate::read_test_bucket_sync();
1501 let obj = Object::create_sync(&bucket.name, vec![0, 1], "test-rewrite", "text/plain")?;
1502 let obj = obj.rewrite_sync(&bucket.name, "test-rewritten")?;
1503 let url = obj.download_url(100)?;
1504 let client = reqwest::blocking::Client::new();
1505 let download = client.head(&url).send()?;
1506 assert_eq!(download.status().as_u16(), 200);
1507 Ok(())
1508 }
1509
1510 #[test]
1511 fn test_url_encoding() -> Result<(), Box<dyn std::error::Error>> {
1512 let bucket = crate::read_test_bucket_sync();
1513 let complicated_names = [
1514 "asdf",
1515 "asdf+1",
1516 "asdf&&+1?=3,,-_()*&^%$#@!`~{}[]\\|:;\"'<>,.?/äöüëß",
1517 "https://www.google.com",
1518 "परिक्षण फाईल",
1519 "测试很重要",
1520 ];
1521 for name in &complicated_names {
1522 let _obj = Object::create_sync(&bucket.name, vec![0, 1], name, "text/plain")?;
1523 let obj = Object::read_sync(&bucket.name, &name).unwrap();
1524 let url = obj.download_url(100)?;
1525 let client = reqwest::blocking::Client::new();
1526 let download = client.head(&url).send()?;
1527 assert_eq!(download.status().as_u16(), 200);
1528 }
1529 Ok(())
1530 }
1531 }
1532}
1533
1534pub struct SizedByteStream<S: Stream<Item = crate::Result<u8>> + Unpin> {
1536 size: Option<u64>,
1537 bytes: S,
1538}
1539
1540impl<S: Stream<Item = crate::Result<u8>> + Unpin> SizedByteStream<S> {
1541 pub(crate) fn new(bytes: S, size: Option<u64>) -> Self {
1542 Self { size, bytes }
1543 }
1544}
1545
1546impl<S: Stream<Item = crate::Result<u8>> + Unpin> Stream for SizedByteStream<S> {
1547 type Item = crate::Result<u8>;
1548
1549 fn poll_next(
1550 mut self: std::pin::Pin<&mut Self>,
1551 cx: &mut std::task::Context,
1552 ) -> std::task::Poll<Option<Self::Item>> {
1553 futures_util::StreamExt::poll_next_unpin(&mut self.bytes, cx)
1554 }
1555
1556 fn size_hint(&self) -> (usize, Option<usize>) {
1557 let size = self
1558 .size
1559 .and_then(|s| std::convert::TryInto::try_into(s).ok());
1560 (size.unwrap_or(0), size)
1561 }
1562}