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