1use std::sync::Arc;
41use std::time::Duration;
42
43use crate::CopyOptions;
44use crate::client::{CredentialProvider, crypto_provider};
45use crate::gcp::credential::GCSAuthorizer;
46use crate::retry::{MultipartRetry, RetryPolicy};
47use crate::signer::{SignedUrlOptions, Signer};
48use crate::util::validate_signed_url_extras;
49use crate::{
50 GetOptions, GetResult, ListResult, MultipartId, MultipartUpload, ObjectMeta, ObjectStore,
51 PutMultipartOptions, PutOptions, PutPayload, PutResult, Result, UploadPart, multipart::PartId,
52 path::Path,
53};
54use async_trait::async_trait;
55use client::GoogleCloudStorageClient;
56use futures_util::stream::{BoxStream, StreamExt};
57use http::Method;
58use url::Url;
59
60use crate::client::get::GetClientExt;
61use crate::client::list::{ListClient, ListClientExt};
62use crate::client::parts::Parts;
63use crate::list::{PaginatedListOptions, PaginatedListResult, PaginatedListStore};
64use crate::multipart::MultipartStore;
65pub use builder::{GoogleCloudStorageBuilder, GoogleConfigKey};
66pub use credential::{GcpCredential, GcpSigningCredential, ServiceAccountKey};
67
68mod builder;
69mod client;
70mod credential;
71
72const STORE: &str = "GCS";
73
74pub type GcpCredentialProvider = Arc<dyn CredentialProvider<Credential = GcpCredential>>;
76
77pub type GcpSigningCredentialProvider =
79 Arc<dyn CredentialProvider<Credential = GcpSigningCredential>>;
80
81#[derive(Debug, Clone)]
83pub struct GoogleCloudStorage {
84 client: Arc<GoogleCloudStorageClient>,
85}
86
87impl std::fmt::Display for GoogleCloudStorage {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(
90 f,
91 "GoogleCloudStorage({})",
92 self.client.config().bucket_name
93 )
94 }
95}
96
97impl GoogleCloudStorage {
98 pub fn credentials(&self) -> &GcpCredentialProvider {
100 &self.client.config().credentials
101 }
102
103 pub fn signing_credentials(&self) -> &GcpSigningCredentialProvider {
105 &self.client.config().signing_credentials
106 }
107}
108
109#[derive(Debug)]
110struct GCSMultipartUpload {
111 state: Arc<UploadState>,
112 part_idx: usize,
113}
114
115#[derive(Debug)]
116struct UploadState {
117 client: Arc<GoogleCloudStorageClient>,
118 path: Path,
119 multipart_id: MultipartId,
120 parts: Parts,
121 retry_policy: Option<Arc<dyn RetryPolicy>>,
122}
123
124#[async_trait]
125impl MultipartUpload for GCSMultipartUpload {
126 fn put_part(&mut self, payload: PutPayload) -> UploadPart {
127 let idx = self.part_idx;
128 self.part_idx += 1;
129 let state = Arc::clone(&self.state);
130 Box::pin(async move {
131 let mut retry = MultipartRetry::new(state.retry_policy.clone());
132 loop {
133 match state
134 .client
135 .put_part(&state.path, &state.multipart_id, idx, payload.clone())
136 .await
137 {
138 Ok(part) => {
139 state.parts.put(idx, part);
140 return Ok(());
141 }
142 Err(error) if retry.should_retry(&error).await => continue,
143 Err(error) => return Err(error),
144 }
145 }
146 })
147 }
148
149 async fn complete(&mut self) -> Result<PutResult> {
150 let parts = self.state.parts.finish(self.part_idx)?;
151
152 self.state
153 .client
154 .multipart_complete(&self.state.path, &self.state.multipart_id, parts)
155 .await
156 }
157
158 async fn abort(&mut self) -> Result<()> {
159 self.state
160 .client
161 .multipart_cleanup(&self.state.path, &self.state.multipart_id)
162 .await
163 }
164}
165
166#[async_trait]
167impl ObjectStore for GoogleCloudStorage {
168 async fn put_opts(
169 &self,
170 location: &Path,
171 payload: PutPayload,
172 opts: PutOptions,
173 ) -> Result<PutResult> {
174 self.client.put(location, payload, opts).await
175 }
176
177 async fn put_multipart_opts(
178 &self,
179 location: &Path,
180 opts: PutMultipartOptions,
181 ) -> Result<Box<dyn MultipartUpload>> {
182 let retry_policy = opts.retry_policy();
183 let upload_id = self.client.multipart_initiate(location, opts).await?;
184
185 Ok(Box::new(GCSMultipartUpload {
186 part_idx: 0,
187 state: Arc::new(UploadState {
188 client: Arc::clone(&self.client),
189 path: location.clone(),
190 multipart_id: upload_id.clone(),
191 parts: Default::default(),
192 retry_policy,
193 }),
194 }))
195 }
196
197 async fn get_opts(&self, location: &Path, options: GetOptions) -> Result<GetResult> {
198 self.client.get_opts(location, options).await
199 }
200
201 fn delete_stream(
202 &self,
203 locations: BoxStream<'static, Result<Path>>,
204 ) -> BoxStream<'static, Result<Path>> {
205 let client = Arc::clone(&self.client);
206 locations
207 .map(move |location| {
208 let client = Arc::clone(&client);
209 async move {
210 let location = location?;
211 client.delete_request(&location).await?;
212 Ok(location)
213 }
214 })
215 .buffered(10)
216 .boxed()
217 }
218
219 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
220 self.client.list(prefix)
221 }
222
223 fn list_with_offset(
224 &self,
225 prefix: Option<&Path>,
226 offset: &Path,
227 ) -> BoxStream<'static, Result<ObjectMeta>> {
228 self.client.list_with_offset(prefix, offset)
229 }
230
231 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> Result<ListResult> {
232 self.client.list_with_delimiter(prefix).await
233 }
234
235 async fn copy_opts(&self, from: &Path, to: &Path, options: CopyOptions) -> Result<()> {
236 let CopyOptions {
237 mode,
238 extensions: _,
239 } = options;
240
241 self.client.copy_request(from, to, mode).await
242 }
243}
244
245#[async_trait]
246impl MultipartStore for GoogleCloudStorage {
247 async fn create_multipart(&self, path: &Path) -> Result<MultipartId> {
300 self.client
301 .multipart_initiate(path, PutMultipartOptions::default())
302 .await
303 }
304
305 async fn create_multipart_opts(
306 &self,
307 path: &Path,
308 opts: PutMultipartOptions,
309 ) -> Result<MultipartId> {
310 self.client.multipart_initiate(path, opts).await
311 }
312
313 async fn put_part(
314 &self,
315 path: &Path,
316 id: &MultipartId,
317 part_idx: usize,
318 payload: PutPayload,
319 ) -> Result<PartId> {
320 self.client.put_part(path, id, part_idx, payload).await
321 }
322
323 async fn complete_multipart(
324 &self,
325 path: &Path,
326 id: &MultipartId,
327 parts: Vec<PartId>,
328 ) -> Result<PutResult> {
329 self.client.multipart_complete(path, id, parts).await
330 }
331
332 async fn abort_multipart(&self, path: &Path, id: &MultipartId) -> Result<()> {
333 self.client.multipart_cleanup(path, id).await
334 }
335}
336
337#[async_trait]
338impl Signer for GoogleCloudStorage {
339 async fn signed_url(&self, method: Method, path: &Path, expires_in: Duration) -> Result<Url> {
340 self.signed_url_opts(method, path, expires_in, &SignedUrlOptions::default())
341 .await
342 }
343
344 async fn signed_url_opts(
351 &self,
352 method: Method,
353 path: &Path,
354 expires_in: Duration,
355 options: &SignedUrlOptions,
356 ) -> Result<Url> {
357 if expires_in.as_secs() > 604800 {
358 return Err(crate::Error::Generic {
359 store: STORE,
360 source: "Expiration Time can't be longer than 604800 seconds (7 days).".into(),
361 });
362 }
363
364 validate_signed_url_extras(
367 STORE,
368 &options.extra_query,
369 &options.signed_headers,
370 "x-goog-",
371 )?;
372
373 let config = self.client.config();
374 let path_url = config.path_url(path);
375 let mut url = Url::parse(&path_url).map_err(|e| crate::Error::Generic {
376 store: STORE,
377 source: format!("Unable to parse url {path_url}: {e}").into(),
378 })?;
379
380 let signing_credentials = self.signing_credentials().get_credential().await?;
381 let authorizer = GCSAuthorizer::new(signing_credentials);
382
383 let crypto = crypto_provider(self.client.config().crypto.as_deref())?;
384 authorizer
385 .sign_with(
386 crypto,
387 method,
388 &mut url,
389 &options.extra_query,
390 &options.signed_headers,
391 expires_in,
392 &self.client,
393 )
394 .await?;
395
396 Ok(url)
397 }
398}
399
400#[async_trait]
401impl PaginatedListStore for GoogleCloudStorage {
402 async fn list_paginated(
403 &self,
404 prefix: Option<&str>,
405 opts: PaginatedListOptions,
406 ) -> Result<PaginatedListResult> {
407 self.client.list_request(prefix, opts).await
408 }
409}
410
411#[cfg(test)]
412mod test {
413 use credential::DEFAULT_GCS_BASE_URL;
414
415 use crate::ObjectStoreExt;
416 use crate::integration::*;
417 use crate::tests::*;
418
419 use super::*;
420
421 const NON_EXISTENT_NAME: &str = "nonexistentname";
422
423 #[tokio::test]
424 async fn gcs_test() {
425 maybe_skip_integration!();
426 let integration = GoogleCloudStorageBuilder::from_env()
429 .with_http_connector(MarkerHttpConnector::default())
430 .build()
431 .unwrap();
432
433 put_get_delete_list(&integration).await;
434 list_with_offset_exclusivity(&integration).await;
435 list_uses_directories_correctly(&integration).await;
436 list_with_delimiter(&integration).await;
437 rename_and_copy(&integration).await;
438 if integration.client.config().base_url == DEFAULT_GCS_BASE_URL {
439 copy_if_not_exists(&integration).await;
442 stream_get(&integration).await;
445 multipart(&integration, &integration).await;
446 multipart_with_opts(&integration, &integration).await;
447 multipart_put_part_out_of_order(&integration, &integration).await;
448 multipart_race_condition(&integration, true).await;
449 multipart_out_of_order(&integration).await;
450 list_paginated(&integration, &integration).await;
451 get_opts(&integration).await;
453 put_opts(&integration, true).await;
454 put_get_attributes(&integration).await;
456 }
457
458 let test_multipart = integration.client.config().base_url == DEFAULT_GCS_BASE_URL;
460 response_extensions(&integration, test_multipart).await;
461 }
462
463 #[cfg(feature = "reqwest")]
464 #[tokio::test]
465 #[ignore]
466 async fn gcs_test_sign() {
467 maybe_skip_integration!();
468 let integration = GoogleCloudStorageBuilder::from_env().build().unwrap();
469
470 let client = reqwest::Client::new();
471
472 let path = Path::from("test_sign");
473 let url = integration
474 .signed_url(Method::PUT, &path, Duration::from_secs(3600))
475 .await
476 .unwrap();
477 println!("PUT {url}");
478
479 let resp = client.put(url).body("data").send().await.unwrap();
480 resp.error_for_status().unwrap();
481
482 let url = integration
483 .signed_url(Method::GET, &path, Duration::from_secs(3600))
484 .await
485 .unwrap();
486 println!("GET {url}");
487
488 let resp = client.get(url).send().await.unwrap();
489 let resp = resp.error_for_status().unwrap();
490 let data = resp.bytes().await.unwrap();
491 assert_eq!(data.as_ref(), b"data");
492 }
493
494 #[tokio::test]
495 async fn gcs_test_get_nonexistent_location() {
496 maybe_skip_integration!();
497 let integration = GoogleCloudStorageBuilder::from_env().build().unwrap();
498
499 let location = Path::from_iter([NON_EXISTENT_NAME]);
500
501 let err = integration.get(&location).await.unwrap_err();
502
503 assert!(
504 matches!(err, crate::Error::NotFound { .. }),
505 "unexpected error type: {err}"
506 );
507 }
508
509 #[tokio::test]
510 async fn gcs_test_get_nonexistent_bucket() {
511 maybe_skip_integration!();
512 let config = GoogleCloudStorageBuilder::from_env();
513 let integration = config.with_bucket_name(NON_EXISTENT_NAME).build().unwrap();
514
515 let location = Path::from_iter([NON_EXISTENT_NAME]);
516
517 let err = get_nonexistent_object(&integration, Some(location))
518 .await
519 .unwrap_err();
520
521 assert!(
522 matches!(err, crate::Error::NotFound { .. }),
523 "unexpected error type: {err}"
524 );
525 }
526
527 #[tokio::test]
528 async fn gcs_test_delete_nonexistent_location() {
529 maybe_skip_integration!();
530 let integration = GoogleCloudStorageBuilder::from_env().build().unwrap();
531
532 let location = Path::from_iter([NON_EXISTENT_NAME]);
533
534 let err = integration.delete(&location).await.unwrap_err();
535 assert!(
536 matches!(err, crate::Error::NotFound { .. }),
537 "unexpected error type: {err}"
538 );
539 }
540
541 #[tokio::test]
542 async fn gcs_test_delete_nonexistent_bucket() {
543 maybe_skip_integration!();
544 let config = GoogleCloudStorageBuilder::from_env();
545 let integration = config.with_bucket_name(NON_EXISTENT_NAME).build().unwrap();
546
547 let location = Path::from_iter([NON_EXISTENT_NAME]);
548
549 let err = integration.delete(&location).await.unwrap_err();
550 assert!(
551 matches!(err, crate::Error::NotFound { .. }),
552 "unexpected error type: {err}"
553 );
554 }
555
556 #[tokio::test]
557 async fn gcs_test_put_nonexistent_bucket() {
558 maybe_skip_integration!();
559 let config = GoogleCloudStorageBuilder::from_env();
560 let integration = config.with_bucket_name(NON_EXISTENT_NAME).build().unwrap();
561
562 let location = Path::from_iter([NON_EXISTENT_NAME]);
563 let data = PutPayload::from("arbitrary data");
564
565 let err = integration
566 .put(&location, data)
567 .await
568 .unwrap_err()
569 .to_string();
570 assert!(
571 err.contains("Server returned non-2xx status code: 404 Not Found"),
572 "{}",
573 err
574 )
575 }
576}