1use std::collections::HashMap;
19use std::fmt::Debug;
20use std::fmt::Formatter;
21use std::fmt::Write;
22use std::sync::Arc;
23use std::time::Duration;
24
25use backon::ExponentialBuilder;
26use backon::Retryable;
27use bytes::{Buf, Bytes};
28use http::header::CONTENT_ENCODING;
29use http::header::CONTENT_LENGTH;
30use http::header::CONTENT_TYPE;
31use http::header::HOST;
32use http::header::IF_MATCH;
33use http::header::IF_MODIFIED_SINCE;
34use http::header::IF_NONE_MATCH;
35use http::header::IF_UNMODIFIED_SINCE;
36use http::Request;
37use http::Response;
38use reqsign::GoogleCredential;
39use reqsign::GoogleCredentialLoader;
40use reqsign::GoogleSigner;
41use reqsign::GoogleToken;
42use reqsign::GoogleTokenLoader;
43use serde::Deserialize;
44use serde::Serialize;
45use std::sync::LazyLock;
46
47use super::uri::percent_encode_path;
48use crate::raw::*;
49use crate::*;
50use constants::*;
51
52pub mod constants {
53 pub const X_GOOG_ACL: &str = "x-goog-acl";
54 pub const X_GOOG_STORAGE_CLASS: &str = "x-goog-storage-class";
55 pub const X_GOOG_META_PREFIX: &str = "x-goog-meta-";
56}
57
58pub struct GcsCore {
59 pub info: Arc<AccessorInfo>,
60 pub endpoint: String,
61 pub bucket: String,
62 pub root: String,
63
64 pub signer: GoogleSigner,
65 pub token_loader: GoogleTokenLoader,
66 pub token: Option<String>,
67 pub scope: String,
68 pub credential_loader: GoogleCredentialLoader,
69
70 pub predefined_acl: Option<String>,
71 pub default_storage_class: Option<String>,
72
73 pub allow_anonymous: bool,
74}
75
76impl Debug for GcsCore {
77 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
78 let mut de = f.debug_struct("Backend");
79 de.field("endpoint", &self.endpoint)
80 .field("bucket", &self.bucket)
81 .field("root", &self.root)
82 .finish_non_exhaustive()
83 }
84}
85
86static BACKOFF: LazyLock<ExponentialBuilder> =
87 LazyLock::new(|| ExponentialBuilder::default().with_jitter());
88
89impl GcsCore {
90 async fn load_token(&self) -> Result<Option<GoogleToken>> {
91 if let Some(token) = &self.token {
92 return Ok(Some(GoogleToken::new(token, usize::MAX, &self.scope)));
93 }
94
95 let cred = { || self.token_loader.load() }
96 .retry(*BACKOFF)
97 .await
98 .map_err(new_request_credential_error)?;
99
100 if let Some(cred) = cred {
101 return Ok(Some(cred));
102 }
103
104 if self.allow_anonymous {
105 return Ok(None);
106 }
107
108 Err(Error::new(
109 ErrorKind::ConfigInvalid,
110 "no valid credential found",
111 ))
112 }
113
114 fn load_credential(&self) -> Result<Option<GoogleCredential>> {
115 let cred = self
116 .credential_loader
117 .load()
118 .map_err(new_request_credential_error)?;
119
120 if let Some(cred) = cred {
121 return Ok(Some(cred));
122 }
123
124 if self.allow_anonymous {
125 return Ok(None);
126 }
127
128 Err(Error::new(
129 ErrorKind::ConfigInvalid,
130 "no valid credential found",
131 ))
132 }
133
134 pub async fn sign<T>(&self, req: &mut Request<T>) -> Result<()> {
135 if let Some(cred) = self.load_token().await? {
136 self.signer
137 .sign(req, &cred)
138 .map_err(new_request_sign_error)?;
139 } else {
140 return Ok(());
141 }
142
143 req.headers_mut().remove(HOST);
150
151 Ok(())
152 }
153
154 pub fn sign_query<T>(&self, req: &mut Request<T>, duration: Duration) -> Result<()> {
155 if let Some(cred) = self.load_credential()? {
156 self.signer
157 .sign_query(req, duration, &cred)
158 .map_err(new_request_sign_error)?;
159 } else {
160 return Ok(());
161 }
162
163 req.headers_mut().remove(HOST);
170
171 Ok(())
172 }
173
174 #[inline]
175 pub async fn send(&self, req: Request<Buffer>) -> Result<Response<Buffer>> {
176 self.info.http_client().send(req).await
177 }
178}
179
180impl GcsCore {
181 pub fn gcs_get_object_request(
182 &self,
183 path: &str,
184 range: BytesRange,
185 args: &OpRead,
186 ) -> Result<Request<Buffer>> {
187 let p = build_abs_path(&self.root, path);
188
189 let url = format!(
190 "{}/storage/v1/b/{}/o/{}?alt=media",
191 self.endpoint,
192 self.bucket,
193 percent_encode_path(&p)
194 );
195
196 let mut req = Request::get(&url);
197
198 if let Some(if_match) = args.if_match() {
199 req = req.header(IF_MATCH, if_match);
200 }
201 if let Some(if_none_match) = args.if_none_match() {
202 req = req.header(IF_NONE_MATCH, if_none_match);
203 }
204 if !range.is_full() {
205 req = req.header(http::header::RANGE, range.to_header());
206 }
207
208 let req = req.extension(Operation::Read);
209
210 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
211
212 Ok(req)
213 }
214
215 pub fn gcs_get_object_xml_request(&self, path: &str, args: &OpRead) -> Result<Request<Buffer>> {
217 let p = build_abs_path(&self.root, path);
218
219 let url = format!("{}/{}/{}", self.endpoint, self.bucket, p);
220
221 let mut req = Request::get(&url);
222
223 if let Some(if_match) = args.if_match() {
224 req = req.header(IF_MATCH, if_match);
225 }
226 if let Some(if_none_match) = args.if_none_match() {
227 req = req.header(IF_NONE_MATCH, if_none_match);
228 }
229
230 if let Some(if_modified_since) = args.if_modified_since() {
231 req = req.header(
232 IF_MODIFIED_SINCE,
233 format_datetime_into_http_date(if_modified_since),
234 );
235 }
236
237 if let Some(if_unmodified_since) = args.if_unmodified_since() {
238 req = req.header(
239 IF_UNMODIFIED_SINCE,
240 format_datetime_into_http_date(if_unmodified_since),
241 );
242 }
243
244 let req = req.extension(Operation::Read);
245
246 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
247
248 Ok(req)
249 }
250
251 pub async fn gcs_get_object(
252 &self,
253 path: &str,
254 range: BytesRange,
255 args: &OpRead,
256 ) -> Result<Response<HttpBody>> {
257 let mut req = self.gcs_get_object_request(path, range, args)?;
258
259 self.sign(&mut req).await?;
260 self.info.http_client().fetch(req).await
261 }
262
263 pub fn gcs_insert_object_request(
264 &self,
265 path: &str,
266 size: Option<u64>,
267 op: &OpWrite,
268 body: Buffer,
269 ) -> Result<Request<Buffer>> {
270 let p = build_abs_path(&self.root, path);
271
272 let request_metadata = InsertRequestMetadata {
273 storage_class: self.default_storage_class.as_deref(),
274 cache_control: op.cache_control(),
275 content_type: op.content_type(),
276 content_encoding: op.content_encoding(),
277 metadata: op.user_metadata(),
278 };
279
280 let mut url = format!(
281 "{}/upload/storage/v1/b/{}/o?uploadType={}&name={}",
282 self.endpoint,
283 self.bucket,
284 if request_metadata.is_empty() {
285 "media"
286 } else {
287 "multipart"
288 },
289 percent_encode_path(&p)
290 );
291
292 if let Some(acl) = &self.predefined_acl {
293 write!(&mut url, "&predefinedAcl={}", acl).unwrap();
294 }
295
296 if op.if_not_exists() {
300 write!(&mut url, "&ifGenerationMatch=0").unwrap();
301 }
302
303 let mut req = Request::post(&url);
304
305 req = req.header(CONTENT_LENGTH, size.unwrap_or_default());
306
307 if request_metadata.is_empty() {
308 let req = req.extension(Operation::Write);
309 let req = req.body(body).map_err(new_request_build_error)?;
313 Ok(req)
314 } else {
315 let mut multipart = Multipart::new();
316 let metadata_part = RelatedPart::new()
317 .header(
318 CONTENT_TYPE,
319 "application/json; charset=UTF-8".parse().unwrap(),
320 )
321 .content(
322 serde_json::to_vec(&request_metadata)
323 .expect("metadata serialization should succeed"),
324 );
325 multipart = multipart.part(metadata_part);
326
327 let content_type = op
329 .content_type()
330 .unwrap_or("application/octet-stream")
331 .parse()
332 .expect("Failed to parse content-type");
333 let media_part = RelatedPart::new()
334 .header(CONTENT_TYPE, content_type)
335 .content(body);
336 multipart = multipart.part(media_part);
337
338 let req = multipart.apply(Request::post(url).extension(Operation::Write))?;
339
340 Ok(req)
341 }
342 }
343
344 pub fn gcs_insert_object_xml_request(
346 &self,
347 path: &str,
348 args: &OpWrite,
349 body: Buffer,
350 ) -> Result<Request<Buffer>> {
351 let p = build_abs_path(&self.root, path);
352
353 let url = format!("{}/{}/{}", self.endpoint, self.bucket, p);
354
355 let mut req = Request::put(&url);
356
357 if let Some(user_metadata) = args.user_metadata() {
358 for (key, value) in user_metadata {
359 req = req.header(format!("{X_GOOG_META_PREFIX}{key}"), value)
360 }
361 }
362
363 if let Some(content_type) = args.content_type() {
364 req = req.header(CONTENT_TYPE, content_type);
365 }
366
367 if let Some(content_encoding) = args.content_encoding() {
368 req = req.header(CONTENT_ENCODING, content_encoding);
369 }
370
371 if let Some(acl) = &self.predefined_acl {
372 req = req.header(X_GOOG_ACL, acl);
373 }
374
375 if let Some(storage_class) = &self.default_storage_class {
376 req = req.header(X_GOOG_STORAGE_CLASS, storage_class);
377 }
378
379 let req = req.extension(Operation::Write);
380
381 let req = req.body(body).map_err(new_request_build_error)?;
382
383 Ok(req)
384 }
385
386 pub fn gcs_head_object_request(&self, path: &str, args: &OpStat) -> Result<Request<Buffer>> {
387 let p = build_abs_path(&self.root, path);
388
389 let url = format!(
390 "{}/storage/v1/b/{}/o/{}",
391 self.endpoint,
392 self.bucket,
393 percent_encode_path(&p)
394 );
395
396 let mut req = Request::get(&url);
397
398 if let Some(if_none_match) = args.if_none_match() {
399 req = req.header(IF_NONE_MATCH, if_none_match);
400 }
401
402 if let Some(if_match) = args.if_match() {
403 req = req.header(IF_MATCH, if_match);
404 }
405
406 let req = req.extension(Operation::Stat);
407
408 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
409
410 Ok(req)
411 }
412
413 pub fn gcs_head_object_xml_request(
415 &self,
416 path: &str,
417 args: &OpStat,
418 ) -> Result<Request<Buffer>> {
419 let p = build_abs_path(&self.root, path);
420
421 let url = format!("{}/{}/{}", self.endpoint, self.bucket, p);
422
423 let mut req = Request::head(&url);
424
425 if let Some(if_none_match) = args.if_none_match() {
426 req = req.header(IF_NONE_MATCH, if_none_match);
427 }
428
429 if let Some(if_match) = args.if_match() {
430 req = req.header(IF_MATCH, if_match);
431 }
432
433 let req = req.extension(Operation::Stat);
434
435 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
436
437 Ok(req)
438 }
439
440 pub async fn gcs_get_object_metadata(
441 &self,
442 path: &str,
443 args: &OpStat,
444 ) -> Result<Response<Buffer>> {
445 let mut req = self.gcs_head_object_request(path, args)?;
446
447 self.sign(&mut req).await?;
448
449 self.send(req).await
450 }
451
452 pub async fn gcs_delete_object(&self, path: &str) -> Result<Response<Buffer>> {
453 let mut req = self.gcs_delete_object_request(path)?;
454
455 self.sign(&mut req).await?;
456 self.send(req).await
457 }
458
459 pub fn gcs_delete_object_request(&self, path: &str) -> Result<Request<Buffer>> {
460 let p = build_abs_path(&self.root, path);
461
462 let url = format!(
463 "{}/storage/v1/b/{}/o/{}",
464 self.endpoint,
465 self.bucket,
466 percent_encode_path(&p)
467 );
468
469 Request::delete(&url)
470 .body(Buffer::new())
471 .map_err(new_request_build_error)
472 }
473
474 pub async fn gcs_delete_objects(&self, paths: Vec<String>) -> Result<Response<Buffer>> {
475 let uri = format!("{}/batch/storage/v1", self.endpoint);
476
477 let mut multipart = Multipart::new();
478
479 for (idx, path) in paths.iter().enumerate() {
480 let req = self.gcs_delete_object_request(path)?;
481
482 multipart = multipart.part(
483 MixedPart::from_request(req).part_header("content-id".parse().unwrap(), idx.into()),
484 );
485 }
486
487 let req = Request::post(uri).extension(Operation::Delete);
488 let mut req = multipart.apply(req)?;
489
490 self.sign(&mut req).await?;
491 self.send(req).await
492 }
493
494 pub async fn gcs_copy_object(&self, from: &str, to: &str) -> Result<Response<Buffer>> {
495 let source = build_abs_path(&self.root, from);
496 let dest = build_abs_path(&self.root, to);
497
498 let req_uri = format!(
499 "{}/storage/v1/b/{}/o/{}/copyTo/b/{}/o/{}",
500 self.endpoint,
501 self.bucket,
502 percent_encode_path(&source),
503 self.bucket,
504 percent_encode_path(&dest)
505 );
506
507 let mut req = Request::post(req_uri)
508 .header(CONTENT_LENGTH, 0)
509 .extension(Operation::Copy)
510 .body(Buffer::new())
511 .map_err(new_request_build_error)?;
512
513 self.sign(&mut req).await?;
514 self.send(req).await
515 }
516
517 pub async fn gcs_list_objects(
518 &self,
519 path: &str,
520 page_token: &str,
521 delimiter: &str,
522 limit: Option<usize>,
523 start_after: Option<String>,
524 ) -> Result<Response<Buffer>> {
525 let p = build_abs_path(&self.root, path);
526
527 let url = format!("{}/storage/v1/b/{}/o", self.endpoint, self.bucket,);
528
529 let mut url = QueryPairsWriter::new(&url);
530 url = url.push("prefix", &percent_encode_path(&p));
531
532 if !delimiter.is_empty() {
533 url = url.push("delimiter", delimiter);
534 }
535 if let Some(limit) = limit {
536 url = url.push("maxResults", &limit.to_string());
537 }
538 if let Some(start_after) = start_after {
539 let start_after = build_abs_path(&self.root, &start_after);
540 url = url.push("startOffset", &percent_encode_path(&start_after));
541 }
542
543 if !page_token.is_empty() {
544 url = url.push("pageToken", &percent_encode_path(page_token));
551 }
552
553 let mut req = Request::get(url.finish())
554 .extension(Operation::List)
555 .body(Buffer::new())
556 .map_err(new_request_build_error)?;
557
558 self.sign(&mut req).await?;
559
560 self.send(req).await
561 }
562
563 pub async fn gcs_initiate_multipart_upload(&self, path: &str) -> Result<Response<Buffer>> {
564 let p = build_abs_path(&self.root, path);
565
566 let url = format!("{}/{}/{}?uploads", self.endpoint, self.bucket, p);
567
568 let mut req = Request::post(&url)
569 .header(CONTENT_LENGTH, 0)
570 .extension(Operation::Write)
571 .body(Buffer::new())
572 .map_err(new_request_build_error)?;
573
574 self.sign(&mut req).await?;
575 self.send(req).await
576 }
577
578 pub async fn gcs_upload_part(
579 &self,
580 path: &str,
581 upload_id: &str,
582 part_number: usize,
583 size: u64,
584 body: Buffer,
585 ) -> Result<Response<Buffer>> {
586 let p = build_abs_path(&self.root, path);
587
588 let url = format!(
589 "{}/{}/{}?partNumber={}&uploadId={}",
590 self.endpoint,
591 self.bucket,
592 percent_encode_path(&p),
593 part_number,
594 percent_encode_path(upload_id)
595 );
596
597 let mut req = Request::put(&url);
598
599 req = req.header(CONTENT_LENGTH, size);
600
601 let req = req.extension(Operation::Write);
602
603 let mut req = req.body(body).map_err(new_request_build_error)?;
604
605 self.sign(&mut req).await?;
606 self.send(req).await
607 }
608
609 pub async fn gcs_complete_multipart_upload(
610 &self,
611 path: &str,
612 upload_id: &str,
613 parts: Vec<CompleteMultipartUploadRequestPart>,
614 ) -> Result<Response<Buffer>> {
615 let p = build_abs_path(&self.root, path);
616
617 let url = format!(
618 "{}/{}/{}?uploadId={}",
619 self.endpoint,
620 self.bucket,
621 percent_encode_path(&p),
622 percent_encode_path(upload_id)
623 );
624
625 let req = Request::post(&url);
626
627 let content = quick_xml::se::to_string(&CompleteMultipartUploadRequest { part: parts })
628 .map_err(new_xml_serialize_error)?;
629 let req = req.header(CONTENT_LENGTH, content.len());
631 let req = req.header(CONTENT_TYPE, "application/xml");
633
634 let req = req.extension(Operation::Write);
635
636 let mut req = req
637 .body(Buffer::from(Bytes::from(content)))
638 .map_err(new_request_build_error)?;
639
640 self.sign(&mut req).await?;
641 self.send(req).await
642 }
643
644 pub async fn gcs_abort_multipart_upload(
645 &self,
646 path: &str,
647 upload_id: &str,
648 ) -> Result<Response<Buffer>> {
649 let p = build_abs_path(&self.root, path);
650
651 let url = format!(
652 "{}/{}/{}?uploadId={}",
653 self.endpoint,
654 self.bucket,
655 percent_encode_path(&p),
656 percent_encode_path(upload_id)
657 );
658
659 let mut req = Request::delete(&url)
660 .extension(Operation::Write)
661 .body(Buffer::new())
662 .map_err(new_request_build_error)?;
663 self.sign(&mut req).await?;
664 self.send(req).await
665 }
666
667 pub fn build_metadata_from_object_response(path: &str, data: Buffer) -> Result<Metadata> {
668 let meta: GetObjectJsonResponse =
669 serde_json::from_reader(data.reader()).map_err(new_json_deserialize_error)?;
670
671 let mut m = Metadata::new(EntryMode::from_path(path));
672
673 m.set_etag(&meta.etag);
674 m.set_content_md5(&meta.md5_hash);
675
676 let size = meta
677 .size
678 .parse::<u64>()
679 .map_err(|e| Error::new(ErrorKind::Unexpected, "parse u64").set_source(e))?;
680 m.set_content_length(size);
681 if !meta.content_type.is_empty() {
682 m.set_content_type(&meta.content_type);
683 }
684
685 if !meta.content_encoding.is_empty() {
686 m.set_content_encoding(&meta.content_encoding);
687 }
688
689 if !meta.cache_control.is_empty() {
690 m.set_cache_control(&meta.cache_control);
691 }
692
693 if !meta.content_disposition.is_empty() {
694 m.set_content_disposition(&meta.content_disposition);
695 }
696
697 if !meta.generation.is_empty() {
698 m.set_version(&meta.generation);
699 }
700
701 m.set_last_modified(parse_datetime_from_rfc3339(&meta.updated)?);
702
703 if !meta.metadata.is_empty() {
704 m.with_user_metadata(meta.metadata);
705 }
706
707 Ok(m)
708 }
709}
710
711#[derive(Debug, Serialize)]
712#[serde(default, rename_all = "camelCase")]
713pub struct InsertRequestMetadata<'a> {
714 #[serde(skip_serializing_if = "Option::is_none")]
715 content_type: Option<&'a str>,
716 #[serde(skip_serializing_if = "Option::is_none")]
717 content_encoding: Option<&'a str>,
718 #[serde(skip_serializing_if = "Option::is_none")]
719 storage_class: Option<&'a str>,
720 #[serde(skip_serializing_if = "Option::is_none")]
721 cache_control: Option<&'a str>,
722 #[serde(skip_serializing_if = "Option::is_none")]
723 metadata: Option<&'a HashMap<String, String>>,
724}
725
726impl InsertRequestMetadata<'_> {
727 pub fn is_empty(&self) -> bool {
728 self.content_type.is_none()
729 && self.content_encoding.is_none()
730 && self.storage_class.is_none()
731 && self.cache_control.is_none()
732 && self.content_encoding.is_none()
734 && self.metadata.is_none()
735 }
736}
737#[derive(Default, Debug, Deserialize)]
741#[serde(default, rename_all = "camelCase")]
742pub struct ListResponse {
743 pub next_page_token: Option<String>,
747 pub prefixes: Vec<String>,
750 pub items: Vec<ListResponseItem>,
752}
753
754#[derive(Default, Debug, Eq, PartialEq, Deserialize)]
755#[serde(default, rename_all = "camelCase")]
756pub struct ListResponseItem {
757 pub name: String,
758 pub size: String,
759 pub etag: String,
761 pub md5_hash: String,
762 pub updated: String,
763 pub content_type: String,
764}
765
766#[derive(Default, Debug, Deserialize)]
768#[serde(default, rename_all = "PascalCase")]
769pub struct InitiateMultipartUploadResult {
770 pub upload_id: String,
771}
772
773#[derive(Default, Debug, Serialize)]
775#[serde(default, rename = "CompleteMultipartUpload", rename_all = "PascalCase")]
776pub struct CompleteMultipartUploadRequest {
777 pub part: Vec<CompleteMultipartUploadRequestPart>,
778}
779
780#[derive(Clone, Default, Debug, Serialize)]
781#[serde(default, rename_all = "PascalCase")]
782pub struct CompleteMultipartUploadRequestPart {
783 #[serde(rename = "PartNumber")]
784 pub part_number: usize,
785 #[serde(rename = "ETag")]
786 pub etag: String,
787}
788
789#[derive(Debug, Default, Deserialize)]
791#[serde(default, rename_all = "camelCase")]
792struct GetObjectJsonResponse {
793 size: String,
797 etag: String,
801 updated: String,
805 md5_hash: String,
809 content_type: String,
813 content_encoding: String,
817 content_disposition: String,
819 cache_control: String,
821 generation: String,
823 metadata: HashMap<String, String>,
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832
833 #[test]
834 fn test_deserialize_get_object_json_response() {
835 let content = r#"{
836 "kind": "storage#object",
837 "id": "example/1.png/1660563214863653",
838 "selfLink": "https://www.googleapis.com/storage/v1/b/example/o/1.png",
839 "mediaLink": "https://content-storage.googleapis.com/download/storage/v1/b/example/o/1.png?generation=1660563214863653&alt=media",
840 "name": "1.png",
841 "bucket": "example",
842 "generation": "1660563214863653",
843 "metageneration": "1",
844 "contentType": "image/png",
845 "contentEncoding": "br",
846 "contentDisposition": "attachment",
847 "cacheControl": "public, max-age=3600",
848 "storageClass": "STANDARD",
849 "size": "56535",
850 "md5Hash": "fHcEH1vPwA6eTPqxuasXcg==",
851 "crc32c": "j/un9g==",
852 "etag": "CKWasoTgyPkCEAE=",
853 "timeCreated": "2022-08-15T11:33:34.866Z",
854 "updated": "2022-08-15T11:33:34.866Z",
855 "timeStorageClassUpdated": "2022-08-15T11:33:34.866Z",
856 "metadata" : {
857 "location" : "everywhere"
858 }
859}"#;
860
861 let meta = GcsCore::build_metadata_from_object_response("1.png", content.into())
862 .expect("parse metadata should not fail");
863
864 assert_eq!(meta.content_length(), 56535);
865 assert_eq!(
866 meta.last_modified(),
867 Some(
868 parse_datetime_from_rfc3339("2022-08-15T11:33:34.866Z")
869 .expect("parse date should not fail")
870 )
871 );
872 assert_eq!(meta.content_md5(), Some("fHcEH1vPwA6eTPqxuasXcg=="));
873 assert_eq!(meta.etag(), Some("CKWasoTgyPkCEAE="));
874 assert_eq!(meta.content_type(), Some("image/png"));
875 assert_eq!(meta.content_encoding(), Some("br"));
876 assert_eq!(meta.content_disposition(), Some("attachment"));
877 assert_eq!(meta.cache_control(), Some("public, max-age=3600"));
878 assert_eq!(meta.version(), Some("1660563214863653"));
879
880 let metadata = HashMap::from_iter([("location".to_string(), "everywhere".to_string())]);
881 assert_eq!(meta.user_metadata(), Some(&metadata));
882 }
883
884 #[test]
885 fn test_deserialize_list_response() {
886 let content = r#"
887 {
888 "kind": "storage#objects",
889 "prefixes": [
890 "dir/",
891 "test/"
892 ],
893 "items": [
894 {
895 "kind": "storage#object",
896 "id": "example/1.png/1660563214863653",
897 "selfLink": "https://www.googleapis.com/storage/v1/b/example/o/1.png",
898 "mediaLink": "https://content-storage.googleapis.com/download/storage/v1/b/example/o/1.png?generation=1660563214863653&alt=media",
899 "name": "1.png",
900 "bucket": "example",
901 "generation": "1660563214863653",
902 "metageneration": "1",
903 "contentType": "image/png",
904 "storageClass": "STANDARD",
905 "size": "56535",
906 "md5Hash": "fHcEH1vPwA6eTPqxuasXcg==",
907 "crc32c": "j/un9g==",
908 "etag": "CKWasoTgyPkCEAE=",
909 "timeCreated": "2022-08-15T11:33:34.866Z",
910 "updated": "2022-08-15T11:33:34.866Z",
911 "timeStorageClassUpdated": "2022-08-15T11:33:34.866Z"
912 },
913 {
914 "kind": "storage#object",
915 "id": "example/2.png/1660563214883337",
916 "selfLink": "https://www.googleapis.com/storage/v1/b/example/o/2.png",
917 "mediaLink": "https://content-storage.googleapis.com/download/storage/v1/b/example/o/2.png?generation=1660563214883337&alt=media",
918 "name": "2.png",
919 "bucket": "example",
920 "generation": "1660563214883337",
921 "metageneration": "1",
922 "contentType": "image/png",
923 "storageClass": "STANDARD",
924 "size": "45506",
925 "md5Hash": "e6LsGusU7pFJZk+114NV1g==",
926 "crc32c": "L00QAg==",
927 "etag": "CIm0s4TgyPkCEAE=",
928 "timeCreated": "2022-08-15T11:33:34.886Z",
929 "updated": "2022-08-15T11:33:34.886Z",
930 "timeStorageClassUpdated": "2022-08-15T11:33:34.886Z"
931 }
932 ]
933}
934 "#;
935
936 let output: ListResponse =
937 serde_json::from_str(content).expect("JSON deserialize must succeed");
938 assert!(output.next_page_token.is_none());
939 assert_eq!(output.items.len(), 2);
940 assert_eq!(output.items[0].name, "1.png");
941 assert_eq!(output.items[0].size, "56535");
942 assert_eq!(output.items[0].md5_hash, "fHcEH1vPwA6eTPqxuasXcg==");
943 assert_eq!(output.items[0].etag, "CKWasoTgyPkCEAE=");
944 assert_eq!(output.items[0].updated, "2022-08-15T11:33:34.866Z");
945 assert_eq!(output.items[1].name, "2.png");
946 assert_eq!(output.items[1].size, "45506");
947 assert_eq!(output.items[1].md5_hash, "e6LsGusU7pFJZk+114NV1g==");
948 assert_eq!(output.items[1].etag, "CIm0s4TgyPkCEAE=");
949 assert_eq!(output.items[1].updated, "2022-08-15T11:33:34.886Z");
950 assert_eq!(output.items[1].content_type, "image/png");
951 assert_eq!(output.prefixes, vec!["dir/", "test/"])
952 }
953
954 #[test]
955 fn test_deserialize_list_response_with_next_page_token() {
956 let content = r#"
957 {
958 "kind": "storage#objects",
959 "prefixes": [
960 "dir/",
961 "test/"
962 ],
963 "nextPageToken": "CgYxMC5wbmc=",
964 "items": [
965 {
966 "kind": "storage#object",
967 "id": "example/1.png/1660563214863653",
968 "selfLink": "https://www.googleapis.com/storage/v1/b/example/o/1.png",
969 "mediaLink": "https://content-storage.googleapis.com/download/storage/v1/b/example/o/1.png?generation=1660563214863653&alt=media",
970 "name": "1.png",
971 "bucket": "example",
972 "generation": "1660563214863653",
973 "metageneration": "1",
974 "contentType": "image/png",
975 "storageClass": "STANDARD",
976 "size": "56535",
977 "md5Hash": "fHcEH1vPwA6eTPqxuasXcg==",
978 "crc32c": "j/un9g==",
979 "etag": "CKWasoTgyPkCEAE=",
980 "timeCreated": "2022-08-15T11:33:34.866Z",
981 "updated": "2022-08-15T11:33:34.866Z",
982 "timeStorageClassUpdated": "2022-08-15T11:33:34.866Z"
983 },
984 {
985 "kind": "storage#object",
986 "id": "example/2.png/1660563214883337",
987 "selfLink": "https://www.googleapis.com/storage/v1/b/example/o/2.png",
988 "mediaLink": "https://content-storage.googleapis.com/download/storage/v1/b/example/o/2.png?generation=1660563214883337&alt=media",
989 "name": "2.png",
990 "bucket": "example",
991 "generation": "1660563214883337",
992 "metageneration": "1",
993 "contentType": "image/png",
994 "storageClass": "STANDARD",
995 "size": "45506",
996 "md5Hash": "e6LsGusU7pFJZk+114NV1g==",
997 "crc32c": "L00QAg==",
998 "etag": "CIm0s4TgyPkCEAE=",
999 "timeCreated": "2022-08-15T11:33:34.886Z",
1000 "updated": "2022-08-15T11:33:34.886Z",
1001 "timeStorageClassUpdated": "2022-08-15T11:33:34.886Z"
1002 }
1003 ]
1004}
1005 "#;
1006
1007 let output: ListResponse =
1008 serde_json::from_str(content).expect("JSON deserialize must succeed");
1009 assert_eq!(output.next_page_token, Some("CgYxMC5wbmc=".to_string()));
1010 assert_eq!(output.items.len(), 2);
1011 assert_eq!(output.items[0].name, "1.png");
1012 assert_eq!(output.items[0].size, "56535");
1013 assert_eq!(output.items[0].md5_hash, "fHcEH1vPwA6eTPqxuasXcg==");
1014 assert_eq!(output.items[0].etag, "CKWasoTgyPkCEAE=");
1015 assert_eq!(output.items[0].updated, "2022-08-15T11:33:34.866Z");
1016 assert_eq!(output.items[1].name, "2.png");
1017 assert_eq!(output.items[1].size, "45506");
1018 assert_eq!(output.items[1].md5_hash, "e6LsGusU7pFJZk+114NV1g==");
1019 assert_eq!(output.items[1].etag, "CIm0s4TgyPkCEAE=");
1020 assert_eq!(output.items[1].updated, "2022-08-15T11:33:34.886Z");
1021 assert_eq!(output.prefixes, vec!["dir/", "test/"])
1022 }
1023}