1use crate::error::{Error, GoogleResponse};
2pub use crate::resources::bucket::Owner;
3use crate::resources::object_access_control::ObjectAccessControl;
4use crate::resources::common::ListResponse;
5use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
6
7#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct Object {
11 pub kind: String,
13 pub id: String,
15 pub self_link: String,
17 pub name: String,
19 pub bucket: String,
21 #[serde(deserialize_with = "crate::from_str")]
23 pub generation: i64,
24 #[serde(deserialize_with = "crate::from_str")]
28 pub metageneration: i64,
29 pub content_type: Option<String>,
32 pub time_created: chrono::DateTime<chrono::Utc>,
34 pub updated: chrono::DateTime<chrono::Utc>,
36 pub time_deleted: Option<chrono::DateTime<chrono::Utc>>,
39 pub temporary_hold: Option<bool>,
41 pub event_based_hold: Option<bool>,
43 pub retention_expiration_time: Option<chrono::DateTime<chrono::Utc>>,
46 pub storage_class: String,
48 pub time_storage_class_updated: chrono::DateTime<chrono::Utc>,
51 #[serde(deserialize_with = "crate::from_str")]
53 pub size: u64,
54 pub md5_hash: Option<String>,
57 pub media_link: String,
59 pub content_encoding: Option<String>,
61 pub content_disposition: Option<String>,
63 pub content_language: Option<String>,
65 pub cache_control: Option<String>,
68 pub metadata: Option<std::collections::HashMap<String, String>>,
70 pub acl: Option<Vec<ObjectAccessControl>>,
74 pub owner: Option<Owner>,
78 pub crc32c: String,
82 #[serde(default, deserialize_with = "crate::from_str_opt")]
87 pub component_count: Option<i32>,
88 pub etag: String,
90 pub customer_encryption: Option<CustomerEncrypton>,
92 pub kms_key_name: Option<String>,
94}
95
96#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct CustomerEncrypton {
100 pub encryption_algorithm: String,
102 pub key_sha256: String,
104}
105
106#[derive(Debug, PartialEq, serde::Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct ComposeRequest {
110 pub kind: String,
112 pub source_objects: Vec<SourceObject>,
114 pub destination: Option<Object>,
116}
117
118#[derive(Debug, PartialEq, serde::Serialize)]
120#[serde(rename_all = "camelCase")]
121pub struct SourceObject {
122 pub name: String,
125 pub generation: Option<i64>,
127 pub object_preconditions: Option<ObjectPrecondition>,
129}
130
131#[derive(Debug, PartialEq, serde::Serialize)]
133#[serde(rename_all = "camelCase")]
134pub struct ObjectPrecondition {
135 pub if_generation_match: i64,
139}
140
141#[derive(Debug, serde::Deserialize)]
142#[serde(rename_all = "camelCase")]
143struct ObjectList {
144 kind: String,
145 items: Vec<Object>,
146}
147
148#[derive(Debug, serde::Deserialize)]
149#[serde(rename_all = "camelCase")]
150struct RewriteResponse {
151 kind: String,
152 total_bytes_rewritten: String,
153 object_size: String,
154 done: bool,
155 resource: Object,
156}
157
158impl Object {
159 pub fn create(
175 bucket: &str,
176 file: &[u8],
177 filename: &str,
178 mime_type: &str,
179 ) -> Result<Self, Error> {
180 use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
181
182 const BASE_URL: &str = "https://www.googleapis.com/upload/storage/v1/b";
184 let client = reqwest::blocking::Client::new();
185 let url = &format!("{}/{}/o?uploadType=media&name={}",
186 BASE_URL,
187 percent_encode(&bucket),
188 percent_encode(&filename),
189 );
190 let mut headers = crate::get_headers()?;
191 headers.insert(CONTENT_TYPE, mime_type.to_string().parse()?);
192 headers.insert(CONTENT_LENGTH, file.len().to_string().parse()?);
193 let response = client
194 .post(url)
195 .headers(headers)
196 .body(file.to_owned())
197 .send()?;
198 if response.status() == 200 {
199 Ok(serde_json::from_str(&response.text()?)?)
200 } else {
201 Err(Error::new(&response.text()?))
202 }
203 }
204
205
206 pub fn create_streamed<R: std::io::Read + Send + 'static>(
221 bucket: &str,
222 file: R,
223 length: u64,
224 filename: &str,
225 mime_type: &str,
226 ) -> Result<Self, Error> {
227 use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
228
229 const BASE_URL: &str = "https://www.googleapis.com/upload/storage/v1/b";
231 let client = reqwest::blocking::Client::new();
232 let url = &format!(
233 "{}/{}/o?uploadType=media&name={}",
234 BASE_URL,
235 percent_encode(&bucket),
236 percent_encode(&filename),
237 );
238 let mut headers = crate::get_headers()?;
239 headers.insert(CONTENT_TYPE, mime_type.to_string().parse()?);
240 headers.insert(CONTENT_LENGTH, length.to_string().parse()?);
241 let body = reqwest::blocking::Body::sized(file, length);
242 let response = client
243 .post(url)
244 .headers(headers)
245 .body(body)
246 .send()?;
247 if response.status() == 200 {
248 Ok(serde_json::from_str(&response.text()?)?)
249 } else {
250 Err(Error::new(&response.text()?))
251 }
252 }
253
254 pub fn list(bucket: &str) -> Result<Vec<Self>, Error> {
265 Self::list_from(bucket, None, None)
266 }
267
268 pub fn list_prefix(bucket: &str, prefix: &str) -> Result<Vec<Self>, Error> {
279 Self::list_from(bucket, Some(prefix), None)
280 }
281
282 fn list_from(bucket: &str, prefix: Option<&str>, page_token: Option<&str>) -> Result<Vec<Self>, Error> {
283 let url = format!("{}/b/{}/o", crate::BASE_URL, percent_encode(bucket));
284 let client = reqwest::blocking::Client::new();
285 let mut query = if let Some(page_token) = page_token {
286 vec![("pageToken", page_token)]
287 } else {
288 vec![]
289 };
290 if let Some(prefix) = prefix {
291 query.push(("prefix", prefix));
292 };
293
294 let result: GoogleResponse<ListResponse<Self>> = client
295 .get(&url)
296 .query(&query)
297 .headers(crate::get_headers()?)
298 .send()?
299 .json()?;
300 match result {
301 GoogleResponse::Success(mut s) => {
302 if let Some(page_token) = s.next_page_token {
303 s.items.extend(Self::list_from(bucket, prefix, Some(&page_token))?.into_iter());
304 }
305 Ok(s.items)
306 },
307 GoogleResponse::Error(e) => Err(e.into()),
308 }
309 }
310
311 pub fn read(bucket: &str, file_name: &str) -> Result<Self, Error> {
322 let url = format!(
323 "{}/b/{}/o/{}",
324 crate::BASE_URL,
325 percent_encode(bucket),
326 percent_encode(file_name),
327 );
328 let client = reqwest::blocking::Client::new();
329 let result: GoogleResponse<Self> = client
330 .get(&url)
331 .headers(crate::get_headers()?)
332 .send()?
333 .json()?;
334 match result {
335 GoogleResponse::Success(s) => Ok(s),
336 GoogleResponse::Error(e) => Err(e.into()),
337 }
338 }
339
340 pub fn download(bucket: &str, file_name: &str) -> Result<Vec<u8>, Error> {
351 let url = format!(
352 "{}/b/{}/o/{}?alt=media",
353 crate::BASE_URL,
354 percent_encode(bucket),
355 percent_encode(file_name),
356 );
357 let client = reqwest::blocking::Client::new();
358 Ok(client
359 .get(&url)
360 .headers(crate::get_headers()?)
361 .send()?
362 .bytes()?.to_vec())
363 }
364
365 pub fn update(&self) -> Result<Self, Error> {
378 let url = format!("{}/b/{}/o/{}",
379 crate::BASE_URL,
380 percent_encode(&self.bucket),
381 percent_encode(&self.name),
382 );
383 let client = reqwest::blocking::Client::new();
384 let result: GoogleResponse<Self> = client
385 .put(&url)
386 .headers(crate::get_headers()?)
387 .json(&self)
388 .send()?
389 .json()?;
390 match result {
391 GoogleResponse::Success(s) => Ok(s),
392 GoogleResponse::Error(e) => Err(e.into()),
393 }
394 }
395
396 pub fn delete(bucket: &str, file_name: &str) -> Result<(), Error> {
407 let url = format!("{}/b/{}/o/{}",
408 crate::BASE_URL,
409 percent_encode(bucket),
410 percent_encode(file_name),
411 );
412 let client = reqwest::blocking::Client::new();
413 let response = client.delete(&url).headers(crate::get_headers()?).send()?;
414 if response.status().is_success() {
415 Ok(())
416 } else {
417 Err(Error::Google(response.json()?))
418 }
419 }
420
421 pub fn compose(
451 bucket: &str,
452 req: &ComposeRequest,
453 destination_object: &str,
454 ) -> Result<Self, Error> {
455 let url = format!(
456 "{}/b/{}/o/{}/compose",
457 crate::BASE_URL,
458 percent_encode(&bucket),
459 percent_encode(&destination_object)
460 );
461 let client = reqwest::blocking::Client::new();
462 let result: GoogleResponse<Self> = client
463 .post(&url)
464 .headers(crate::get_headers()?)
465 .json(req)
466 .send()?
467 .json()?;
468 match result {
469 GoogleResponse::Success(s) => Ok(s),
470 GoogleResponse::Error(e) => Err(e.into()),
471 }
472 }
473
474 pub fn copy(&self, destination_bucket: &str, path: &str) -> Result<Self, Error> {
487 use reqwest::header::CONTENT_LENGTH;
488
489 let url = format!(
490 "{base}/b/{sBucket}/o/{sObject}/copyTo/b/{dBucket}/o/{dObject}",
491 base=crate::BASE_URL,
492 sBucket=percent_encode(&self.bucket),
493 sObject=percent_encode(&self.name),
494 dBucket=percent_encode(&destination_bucket),
495 dObject=percent_encode(&path),
496 );
497 let client = reqwest::blocking::Client::new();
498 let mut headers = crate::get_headers()?;
499 headers.insert(CONTENT_LENGTH, "0".parse()?);
500 let result: GoogleResponse<Self> = client.post(&url).headers(headers).send()?.json()?;
501 match result {
502 GoogleResponse::Success(s) => Ok(s),
503 GoogleResponse::Error(e) => Err(e.into()),
504 }
505 }
506
507 pub fn rewrite(&self, destination_bucket: &str, path: &str) -> Result<Self, Error> {
527 use reqwest::header::CONTENT_LENGTH;
528
529 let url = format!(
530 "{base}/b/{sBucket}/o/{sObject}/rewriteTo/b/{dBucket}/o/{dObject}",
531 base=crate::BASE_URL,
532 sBucket=percent_encode(&self.bucket),
533 sObject=percent_encode(&self.name),
534 dBucket=percent_encode(destination_bucket),
535 dObject=percent_encode(path),
536 );
537 let client = reqwest::blocking::Client::new();
538 let mut headers = crate::get_headers()?;
539 headers.insert(CONTENT_LENGTH, "0".parse()?);
540 let result: GoogleResponse<RewriteResponse> =
541 client.post(&url).headers(headers).send()?.json()?;
542 match result {
543 GoogleResponse::Success(s) => Ok(s.resource),
544 GoogleResponse::Error(e) => Err(e.into()),
545 }
546 }
547
548 pub fn download_url(&self, duration: u32) -> Result<String, Error> {
564 self.sign(&self.name, duration, "GET")
565 }
566
567 #[inline(always)]
575 fn sign(&self, file_path: &str, duration: u32, http_verb: &str) -> Result<String, Error> {
576 use openssl::sha;
577
578 if duration > 604800 {
579 let msg = format!("duration may not be greater than 604800, but was {}", duration);
580 return Err(Error::Other(msg));
581 }
582
583 let issue_date = chrono::Utc::now();
585 let file_path = self.path_to_resource(file_path);
586 let query_string = Self::get_canonical_query_string(&issue_date, duration);
587 let canonical_request = self.get_canonical_request(&file_path, &query_string, http_verb);
588
589 let hash = sha::sha256(canonical_request.as_bytes());
591 let hex_hash = hex::encode(hash);
592
593 let string_to_sign = format!(
595 "{signing_algorithm}\n\
596 {current_datetime}\n\
597 {credential_scope}\n\
598 {hashed_canonical_request}",
599 signing_algorithm="GOOG4-RSA-SHA256",
600 current_datetime=issue_date.format("%Y%m%dT%H%M%SZ"),
601 credential_scope=Self::get_credential_scope(&issue_date),
602 hashed_canonical_request=hex_hash,
603 );
604
605 let buffer = Self::sign_str(&string_to_sign);
607 let signature = hex::encode(&buffer?);
608
609 Ok(format!(
611 "https://storage.googleapis.com{path_to_resource}?\
612 {query_string}&\
613 X-Goog-Signature={request_signature}",
614 path_to_resource=file_path,
615 query_string=query_string,
616 request_signature=signature,
617 ))
618 }
619
620 #[inline(always)]
621 fn get_canonical_request(&self, path: &str, query_string: &str, http_verb: &str) -> String {
622 format!(
623 "{http_verb}\n\
624 {path_to_resource}\n\
625 {canonical_query_string}\n\
626 {canonical_headers}\n\
627 \n\
628 {signed_headers}\n\
629 {payload}",
630 http_verb=http_verb,
631 path_to_resource=path,
632 canonical_query_string=query_string,
633 canonical_headers="host:storage.googleapis.com",
634 signed_headers="host",
635 payload="UNSIGNED-PAYLOAD",
636 )
637 }
638
639 #[inline(always)]
640 fn get_canonical_query_string(date: &chrono::DateTime<chrono::Utc>, exp: u32) -> String {
641 let credential = format!(
642 "{authorizer}/{scope}",
643 authorizer=crate::SERVICE_ACCOUNT.client_email,
644 scope=Self::get_credential_scope(date),
645 );
646 format!(
647 "X-Goog-Algorithm={algo}&\
648 X-Goog-Credential={cred}&\
649 X-Goog-Date={date}&\
650 X-Goog-Expires={exp}&\
651 X-Goog-SignedHeaders={signed}",
652 algo="GOOG4-RSA-SHA256",
653 cred=percent_encode(&credential),
654 date=date.format("%Y%m%dT%H%M%SZ"),
655 exp=exp,
656 signed="host",
657 )
658 }
659
660 #[inline(always)]
661 fn path_to_resource(&self, path: &str) -> String {
662 format!(
663 "/{bucket}/{file_path}",
664 bucket=self.bucket,
665 file_path=percent_encode_noslash(path),
666 )
667 }
668
669 #[inline(always)]
670 fn get_credential_scope(date: &chrono::DateTime<chrono::Utc>) -> String {
671 format!("{}/henk/storage/goog4_request", date.format("%Y%m%d"))
672 }
673
674 #[inline(always)]
675 fn sign_str(message: &str) -> Result<Vec<u8>, Error> {
676 use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer};
677
678 let key = PKey::private_key_from_pem(crate::SERVICE_ACCOUNT.private_key.as_bytes())?;
679 let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
680 signer.update(message.as_bytes())?;
681 Ok(signer.sign_to_vec()?)
682 }
683}
684
685const ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC.remove(b'*').remove(b'-').remove(b'.').remove(b'_');
686
687const NOSLASH_ENCODE_SET: &AsciiSet = &ENCODE_SET.remove(b'/').remove(b'~');
688
689fn percent_encode_noslash(input: &str) -> String {
692 utf8_percent_encode(input, NOSLASH_ENCODE_SET).to_string()
693}
694
695fn percent_encode(input: &str) -> String {
696 utf8_percent_encode(input, ENCODE_SET).to_string()
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn create() -> Result<(), Box<dyn std::error::Error>> {
705 let bucket = crate::read_test_bucket();
706 Object::create(&bucket.name, &[0, 1], "test-create", "text/plain")?;
707 Ok(())
708 }
709
710 #[test]
711 fn create_streamed() -> Result<(), Box<dyn std::error::Error>> {
712 let bucket = crate::read_test_bucket();
713 let cursor = std::io::Cursor::new([0, 1]);
714 Object::create_streamed(&bucket.name, cursor, 2, "test-create-streamed", "text/plain")?;
715 Ok(())
716 }
717
718 #[test]
719 fn list() -> Result<(), Box<dyn std::error::Error>> {
720 let test_bucket = crate::read_test_bucket();
721 Object::list(&test_bucket.name)?;
722 Ok(())
723 }
724
725 #[test]
726 fn list_prefix() -> Result<(), Box<dyn std::error::Error>> {
727 let test_bucket = crate::read_test_bucket();
728
729 let prefix_names = [
730 "test-list-prefix/1",
731 "test-list-prefix/2",
732 "test-list-prefix/sub/1",
733 "test-list-prefix/sub/2",
734 ];
735
736 for name in &prefix_names {
737 Object::create(&test_bucket.name, &[0, 1], name, "text/plain")?;
738 }
739
740 let list = Object::list_prefix(&test_bucket.name, "test-list-prefix/")?;
741 assert_eq!(list.len(), 4);
742 let list = Object::list_prefix(&test_bucket.name, "test-list-prefix/sub")?;
743 assert_eq!(list.len(), 2);
744 Ok(())
745 }
746
747
748 #[test]
749 fn read() -> Result<(), Box<dyn std::error::Error>> {
750 let bucket = crate::read_test_bucket();
751 Object::create(&bucket.name, &[0, 1], "test-read", "text/plain")?;
752 Object::read(&bucket.name, "test-read")?;
753 Ok(())
754 }
755
756 #[test]
757 fn download() -> Result<(), Box<dyn std::error::Error>> {
758 let bucket = crate::read_test_bucket();
759 let content = b"hello world";
760 Object::create(&bucket.name, content, "test-download", "application/octet-stream")?;
761
762 let data = Object::download(&bucket.name, "test-download")?;
763 assert_eq!(data, content);
764
765 Ok(())
766 }
767
768 #[test]
769 fn update() -> Result<(), Box<dyn std::error::Error>> {
770 let bucket = crate::read_test_bucket();
771 let mut obj = Object::create(&bucket.name, &[0, 1], "test-update", "text/plain")?;
772 obj.content_type = Some("application/xml".to_string());
773 obj.update()?;
774 Ok(())
775 }
776
777 #[test]
778 fn delete() -> Result<(), Box<dyn std::error::Error>> {
779 let bucket = crate::read_test_bucket();
780 Object::create(&bucket.name, &[0, 1], "test-delete", "text/plain")?;
781
782 Object::delete(&bucket.name, "test-delete")?;
783
784 let list = Object::list_prefix(&bucket.name, "test-delete")?;
785 assert!(list.is_empty());
786
787 Ok(())
788 }
789
790 #[test]
791 fn delete_nonexistent() -> Result<(), Box<dyn std::error::Error>> {
792 let bucket = crate::read_test_bucket();
793
794 let nonexistent_object = "test-delete-nonexistent";
795
796 let delete_result = Object::delete(&bucket.name, nonexistent_object);
797
798 if let Err(Error::Google(google_error_response)) = delete_result {
799 assert!(google_error_response.to_string().contains(
800 &format!("No such object: {}/{}", bucket.name, nonexistent_object)));
801 } else {
802 panic!("Expected a Google error, instead got {:?}", delete_result);
803 }
804
805 Ok(())
806 }
807
808 #[test]
809 fn compose() -> Result<(), Box<dyn std::error::Error>> {
810 let bucket = crate::read_test_bucket();
811 let obj1 = Object::create(&bucket.name, &[0, 1], "test-compose-1", "text/plain")?;
812 let obj2 = Object::create(&bucket.name, &[2, 3], "test-compose-2", "text/plain")?;
813 let compose_request = ComposeRequest {
814 kind: "storage#composeRequest".to_string(),
815 source_objects: vec![
816 SourceObject {
817 name: obj1.name.clone(),
818 generation: None,
819 object_preconditions: None,
820 },
821 SourceObject {
822 name: obj2.name.clone(),
823 generation: None,
824 object_preconditions: None,
825 },
826 ],
827 destination: None,
828 };
829 let obj3 = Object::compose(&bucket.name, &compose_request, "test-concatted-file")?;
830 let url = obj3.download_url(100)?;
831 let content = reqwest::blocking::get(&url)?.text()?;
832 assert_eq!(content.as_bytes(), &[0, 1, 2, 3]);
833 Ok(())
834 }
835
836 #[test]
837 fn copy() -> Result<(), Box<dyn std::error::Error>> {
838 let bucket = crate::read_test_bucket();
839 let original = Object::create(&bucket.name, &[2, 3], "test-copy", "text/plain")?;
840 original.copy(&bucket.name, "test-copy - copy")?;
841 Ok(())
842 }
843
844 #[test]
845 fn rewrite() -> Result<(), Box<dyn std::error::Error>> {
846 let bucket = crate::read_test_bucket();
847 let obj = Object::create(&bucket.name, &[0, 1], "test-rewrite", "text/plain")?;
848 let obj = obj.rewrite(&bucket.name, "test-rewritten")?;
849 let url = obj.download_url(100)?;
850 let client = reqwest::blocking::Client::new();
851 let download = client.head(&url).send()?;
852 assert_eq!(download.status().as_u16(), 200);
853 Ok(())
854 }
855
856 #[test]
857 fn test_url_encoding() -> Result<(), Box<dyn std::error::Error>> {
858 let bucket = crate::read_test_bucket();
859 let complicated_names = [
860 "asdf",
861 "asdf+1",
862 "asdf&&+1?=3,,-_()*&^%$#@!`~{}[]\\|:;\"'<>,.?/äöüëß",
863 "https://www.google.com",
864 "परिक्षण फाईल",
865 "测试很重要",
866 ];
867 for name in &complicated_names {
868 let _obj = Object::create(&bucket.name, &[0, 1], name, "text/plain")?;
869 let obj = Object::read(&bucket.name, &name).unwrap();
870 let url = obj.download_url(100)?;
871 let client = reqwest::blocking::Client::new();
872 let download = client.head(&url).send()?;
873 assert_eq!(download.status().as_u16(), 200);
874 }
875 Ok(())
876 }
877}