Skip to main content

opendal_service_gcs/
backend.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::fmt::Debug;
19use std::sync::Arc;
20
21use log::debug;
22use reqsign_core::Context;
23use reqsign_core::Env as _;
24use reqsign_core::OsEnv;
25use reqsign_core::ProvideCredential;
26use reqsign_core::ProvideCredentialChain;
27use reqsign_core::Signer;
28use reqsign_core::StaticEnv;
29use reqsign_file_read_tokio::TokioFileRead;
30use reqsign_google::Credential;
31use reqsign_google::DefaultCredentialProvider;
32use reqsign_google::FileCredentialProvider;
33use reqsign_google::RequestSigner;
34use reqsign_google::StaticCredentialProvider;
35use reqsign_google::TokenCredentialProvider;
36use reqsign_google::VmMetadataCredentialProvider;
37
38use super::GCS_SCHEME;
39use super::composer::GcsComposer;
40use super::config::GcsConfig;
41use super::copier::GcsCopier;
42use super::core::constants::GCS_REWRITE_MAX_CHUNK_SIZE;
43use super::core::constants::GCS_REWRITE_MIN_CHUNK_SIZE;
44use super::core::parse_error;
45use super::core::*;
46use super::deleter::GcsDeleter;
47use super::lister::GcsLister;
48use super::reader::*;
49use super::writer::GcsConditionalWriter;
50use super::writer::GcsWriter;
51use super::writer::GcsWriters;
52use opendal_core::raw::*;
53use opendal_core::*;
54
55const DEFAULT_GCS_ENDPOINT: &str = "https://storage.googleapis.com";
56const DEFAULT_GCS_SCOPE: &str = "https://www.googleapis.com/auth/devstorage.read_write";
57
58/// [Google Cloud Storage](https://cloud.google.com/storage) services support.
59#[doc = include_str!("docs.md")]
60#[derive(Default)]
61pub struct GcsBuilder {
62    pub(super) config: GcsConfig,
63    pub(super) credential_provider_chain: Option<ProvideCredentialChain<Credential>>,
64}
65
66impl Debug for GcsBuilder {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("GcsBuilder")
69            .field("config", &self.config)
70            .finish_non_exhaustive()
71    }
72}
73
74impl GcsBuilder {
75    /// set the working directory root of backend
76    pub fn root(mut self, root: &str) -> Self {
77        self.config.root = if root.is_empty() {
78            None
79        } else {
80            Some(root.to_string())
81        };
82
83        self
84    }
85
86    /// set the container's name
87    pub fn bucket(mut self, bucket: &str) -> Self {
88        self.config.bucket = bucket.to_string();
89        self
90    }
91
92    /// set the GCS service scope
93    ///
94    /// If not set, we will use `https://www.googleapis.com/auth/devstorage.read_write`.
95    ///
96    /// # Valid scope examples
97    ///
98    /// - read-only: `https://www.googleapis.com/auth/devstorage.read_only`
99    /// - read-write: `https://www.googleapis.com/auth/devstorage.read_write`
100    /// - full-control: `https://www.googleapis.com/auth/devstorage.full_control`
101    ///
102    /// Reference: [Cloud Storage authentication](https://cloud.google.com/storage/docs/authentication)
103    pub fn scope(mut self, scope: &str) -> Self {
104        if !scope.is_empty() {
105            self.config.scope = Some(scope.to_string())
106        };
107        self
108    }
109
110    /// Set the GCS service account.
111    ///
112    /// service account will be used for fetch token from vm metadata.
113    /// If not set, we will try to fetch with `default` service account.
114    pub fn service_account(mut self, service_account: &str) -> Self {
115        if !service_account.is_empty() {
116            self.config.service_account = Some(service_account.to_string())
117        };
118        self
119    }
120
121    /// set the endpoint GCS service uses
122    pub fn endpoint(mut self, endpoint: &str) -> Self {
123        if !endpoint.is_empty() {
124            self.config.endpoint = Some(endpoint.to_string())
125        };
126        self
127    }
128
129    /// set the base64 hashed credentials string used for OAuth2 authentication.
130    ///
131    /// this method allows to specify the credentials directly as a base64 hashed string.
132    /// alternatively, you can use `credential_path()` to provide the local path to a credentials file.
133    /// we will use one of `credential` and `credential_path` to complete the OAuth2 authentication.
134    ///
135    /// Reference: [Google Cloud Storage Authentication](https://cloud.google.com/docs/authentication).
136    pub fn credential(mut self, credential: &str) -> Self {
137        if !credential.is_empty() {
138            self.config.credential = Some(credential.to_string())
139        };
140        self
141    }
142
143    /// set the local path to credentials file which is used for OAuth2 authentication.
144    ///
145    /// credentials file contains the original credentials that have not been base64 hashed.
146    /// we will use one of `credential` and `credential_path` to complete the OAuth2 authentication.
147    ///
148    /// Reference: [Google Cloud Storage Authentication](https://cloud.google.com/docs/authentication).
149    pub fn credential_path(mut self, path: &str) -> Self {
150        if !path.is_empty() {
151            self.config.credential_path = Some(path.to_string())
152        };
153        self
154    }
155
156    /// Specify a customized credential provider used by this service.
157    ///
158    /// This provider will be pushed to the front of credential chain.
159    pub fn credential_provider(
160        mut self,
161        provider: impl ProvideCredential<Credential = Credential> + 'static,
162    ) -> Self {
163        let chain = self.credential_provider_chain.unwrap_or_default();
164        self.credential_provider_chain = Some(chain.push_front(provider));
165        self
166    }
167
168    /// Specify a customized credential provider chain used by this service.
169    ///
170    /// This chain will be pushed to the front of default chain.
171    pub fn credential_provider_chain(mut self, chain: ProvideCredentialChain<Credential>) -> Self {
172        self.credential_provider_chain = Some(chain);
173        self
174    }
175
176    /// Provide the OAuth2 token to use.
177    pub fn token(mut self, token: String) -> Self {
178        self.config.token = Some(token);
179        self
180    }
181
182    /// Disable attempting to load credentials from the GCE metadata server.
183    pub fn disable_vm_metadata(mut self) -> Self {
184        self.config.disable_vm_metadata = true;
185        self
186    }
187
188    /// Disable loading configuration from the environment.
189    pub fn disable_config_load(mut self) -> Self {
190        self.config.disable_config_load = true;
191        self
192    }
193
194    /// Set the predefined acl for GCS.
195    ///
196    /// Available values are:
197    /// - `authenticatedRead`
198    /// - `bucketOwnerFullControl`
199    /// - `bucketOwnerRead`
200    /// - `private`
201    /// - `projectPrivate`
202    /// - `publicRead`
203    pub fn predefined_acl(mut self, acl: &str) -> Self {
204        if !acl.is_empty() {
205            self.config.predefined_acl = Some(acl.to_string())
206        };
207        self
208    }
209
210    /// Set the default storage class for GCS.
211    ///
212    /// Available values are:
213    /// - `STANDARD`
214    /// - `NEARLINE`
215    /// - `COLDLINE`
216    /// - `ARCHIVE`
217    pub fn default_storage_class(mut self, class: &str) -> Self {
218        if !class.is_empty() {
219            self.config.default_storage_class = Some(class.to_string())
220        };
221        self
222    }
223
224    /// Skip signature will skip loading credentials and signing requests.
225    ///
226    /// This is typically used for buckets which are open to the public or GCS
227    /// storage emulators.
228    pub fn skip_signature(mut self) -> Self {
229        self.config.skip_signature = true;
230        self
231    }
232
233    /// Allow anonymous requests.
234    #[deprecated(
235        since = "0.57.0",
236        note = "Please use `skip_signature` instead of `allow_anonymous`"
237    )]
238    pub fn allow_anonymous(self) -> Self {
239        self.skip_signature()
240    }
241}
242
243impl Builder for GcsBuilder {
244    type Config = GcsConfig;
245
246    fn build(self) -> Result<impl Service> {
247        debug!("backend build started: {self:?}");
248
249        #[allow(deprecated)]
250        let skip_signature = self.config.skip_signature || self.config.allow_anonymous;
251
252        let root = normalize_root(&self.config.root.unwrap_or_default());
253        debug!("backend use root {root}");
254
255        // Handle endpoint and bucket name
256        let bucket = match self.config.bucket.is_empty() {
257            false => Ok(&self.config.bucket),
258            true => Err(
259                Error::new(ErrorKind::ConfigInvalid, "The bucket is misconfigured")
260                    .with_operation("Builder::build")
261                    .with_context("service", GCS_SCHEME),
262            ),
263        }?;
264
265        // TODO: server side encryption
266
267        let endpoint = self
268            .config
269            .endpoint
270            .clone()
271            .unwrap_or_else(|| DEFAULT_GCS_ENDPOINT.to_string());
272        debug!("backend use endpoint: {endpoint}");
273
274        let scope = self
275            .config
276            .scope
277            .clone()
278            .unwrap_or_else(|| DEFAULT_GCS_SCOPE.to_string());
279
280        let os_env = OsEnv;
281        let mut envs = os_env.vars();
282        envs.insert("GOOGLE_SCOPE".to_string(), scope.clone());
283
284        let ctx = Context::new()
285            .with_file_read(TokioFileRead)
286            .with_env(StaticEnv {
287                home_dir: os_env.home_dir(),
288                envs,
289            });
290
291        let mut default_credential = DefaultCredentialProvider::builder();
292        #[cfg(target_arch = "wasm32")]
293        {
294            default_credential = default_credential.no_env().no_well_known();
295        }
296
297        if self.config.disable_config_load {
298            default_credential = default_credential.no_env().no_well_known();
299        }
300
301        if self.config.disable_vm_metadata || self.config.service_account.is_some() {
302            default_credential = default_credential.no_vm_metadata();
303        }
304
305        let mut credential_chain = ProvideCredentialChain::new().push(default_credential.build());
306
307        if !self.config.disable_vm_metadata
308            && let Some(service_account) = self.config.service_account.as_deref()
309        {
310            credential_chain = credential_chain.push(
311                VmMetadataCredentialProvider::new()
312                    .with_scope(&scope)
313                    .with_service_account(service_account),
314            );
315        }
316
317        if let Some(path) = self.config.credential_path.as_deref() {
318            credential_chain =
319                credential_chain.push_front(FileCredentialProvider::new(path).with_scope(&scope));
320        }
321
322        if let Some(content) = self.config.credential.as_deref()
323            && let Ok(provider) = StaticCredentialProvider::from_base64(content)
324        {
325            credential_chain = credential_chain.push_front(provider.with_scope(&scope));
326        }
327
328        if let Some(token) = self.config.token.as_deref() {
329            credential_chain = credential_chain.push_front(TokenCredentialProvider::new(token));
330        }
331
332        if let Some(customized_credential_chain) = self.credential_provider_chain {
333            credential_chain = credential_chain.push_front(customized_credential_chain);
334        }
335
336        let sign_ctx = ctx;
337        let signer = Signer::new(
338            sign_ctx.clone(),
339            credential_chain,
340            RequestSigner::new("storage").with_scope(&scope),
341        );
342
343        let info = ServiceInfo::new(GCS_SCHEME, &root, bucket);
344        let capability = Capability {
345            stat: true,
346            stat_with_if_match: true,
347            stat_with_if_none_match: true,
348            stat_with_if_version_match: true,
349            stat_with_if_version_not_match: true,
350
351            read: true,
352            read_with_suffix: true,
353
354            read_with_if_match: true,
355            read_with_if_none_match: true,
356            read_with_if_version_match: true,
357            read_with_if_version_not_match: true,
358
359            write: true,
360            write_can_empty: true,
361            write_can_multi: true,
362            write_with_cache_control: true,
363            write_with_content_type: true,
364            write_with_content_encoding: true,
365            write_with_user_metadata: true,
366            write_with_if_not_exists: true,
367            write_with_if_version_match: true,
368            write_with_if_version_not_match: true,
369
370            // The min multipart size of Gcs is 5 MiB.
371            //
372            // ref: <https://cloud.google.com/storage/docs/xml-api/put-object-multipart>
373            write_multi_min_size: Some(5 * 1024 * 1024),
374            // The max multipart size of Gcs is 5 GiB.
375            //
376            // ref: <https://cloud.google.com/storage/docs/xml-api/put-object-multipart>
377            write_multi_max_size: if cfg!(target_pointer_width = "64") {
378                Some(5 * 1024 * 1024 * 1024)
379            } else {
380                Some(usize::MAX)
381            },
382
383            delete: true,
384            delete_with_if_version_match: true,
385            delete_with_if_version_not_match: true,
386            delete_max_size: Some(100),
387
388            copy: true,
389            copy_with_if_version_match: true,
390            copy_with_if_version_not_match: true,
391            copy_can_multi: true,
392            // GCS rewrite requires maxBytesRewrittenPerCall to be an
393            // integral multiple of 1 MiB if specified.
394            //
395            // ref: <https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite>
396            copy_multi_min_size: Some(GCS_REWRITE_MIN_CHUNK_SIZE),
397            copy_multi_max_size: Some(GCS_REWRITE_MAX_CHUNK_SIZE),
398
399            compose: true,
400            compose_with_content_type: true,
401            compose_with_content_disposition: true,
402            compose_with_content_encoding: true,
403            compose_with_cache_control: true,
404            compose_with_user_metadata: true,
405            compose_with_if_not_exists: true,
406            compose_with_if_version_match: true,
407            compose_with_source_version: true,
408
409            list: true,
410            list_with_limit: true,
411            list_with_start_after: true,
412            list_with_recursive: true,
413
414            presign: true,
415            presign_stat: true,
416            presign_read: true,
417            presign_write: true,
418
419            shared: true,
420
421            ..Default::default()
422        };
423
424        let backend = GcsBackend {
425            core: Arc::new(GcsCore {
426                info,
427                capability,
428                endpoint,
429                bucket: bucket.to_string(),
430                root,
431                signer,
432                sign_ctx,
433                predefined_acl: self.config.predefined_acl.clone(),
434                default_storage_class: self.config.default_storage_class.clone(),
435                skip_signature,
436            }),
437        };
438
439        Ok(backend)
440    }
441}
442
443/// GCS storage backend
444#[derive(Clone, Debug)]
445pub struct GcsBackend {
446    pub(crate) core: Arc<GcsCore>,
447}
448
449impl Service for GcsBackend {
450    type Reader = oio::StreamReader<GcsReader>;
451    type Writer = GcsWriters;
452    type Lister = oio::PageLister<GcsLister>;
453    type Deleter = oio::BatchDeleter<GcsDeleter>;
454    type Copier = GcsCopier;
455    type Composer = GcsComposer;
456
457    fn info(&self) -> ServiceInfo {
458        self.core.info.clone()
459    }
460
461    fn capability(&self) -> Capability {
462        self.core.capability
463    }
464
465    async fn create_dir(
466        &self,
467        _ctx: &OperationContext,
468        _path: &str,
469        _args: OpCreateDir,
470    ) -> Result<RpCreateDir> {
471        Err(Error::new(
472            ErrorKind::Unsupported,
473            "operation is not supported",
474        ))
475    }
476
477    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
478        let error_ctx = ErrorContext::new(ServiceOperation("GetObject"))
479            .with_caller_condition(args.is_conditional());
480        let resp = self.core.gcs_get_object_metadata(ctx, path, &args).await?;
481
482        if !resp.status().is_success() {
483            return Err(parse_error(error_ctx, resp));
484        }
485
486        let slc = resp.into_body();
487        let m = GcsCore::build_metadata_from_object_response(path, slc)?;
488
489        Ok(RpStat::new(m))
490    }
491    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
492        let output: oio::StreamReader<GcsReader> = {
493            Ok(oio::StreamReader::new(GcsReader::new(
494                self.clone(),
495                ctx.clone(),
496                path,
497                args,
498            )))
499        }?;
500
501        Ok(output)
502    }
503
504    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
505        let conditional = args.if_not_exists()
506            || args.if_version_match().is_some()
507            || args.if_version_not_match().is_some();
508        let output: GcsWriters = if conditional {
509            TwoWays::Two(GcsConditionalWriter::new(
510                self.core.clone(),
511                ctx.clone(),
512                path,
513                args,
514            ))
515        } else {
516            let concurrent = args.concurrent();
517            let w = GcsWriter::new(self.core.clone(), ctx.clone(), path, args);
518            // Multipart uploads schedule work through the operation executor
519            // supplied by the caller.
520            let w = oio::MultipartWriter::new(ctx.executor().clone(), w, concurrent);
521            TwoWays::One(w)
522        };
523
524        Ok(output)
525    }
526
527    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
528        let output: oio::BatchDeleter<GcsDeleter> = {
529            Ok(oio::BatchDeleter::new(
530                GcsDeleter::new(self.core.clone(), ctx.clone()),
531                self.core.capability.delete_max_size,
532            ))
533        }?;
534
535        Ok(output)
536    }
537
538    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
539        let output: oio::PageLister<GcsLister> = {
540            let l = GcsLister::new(
541                self.core.clone(),
542                ctx.clone(),
543                path,
544                args.recursive(),
545                args.limit(),
546                args.start_after(),
547            );
548
549            Ok(oio::PageLister::new(l))
550        }?;
551
552        Ok(output)
553    }
554
555    fn copy(
556        &self,
557        ctx: &OperationContext,
558        from: &str,
559        to: &str,
560        args: OpCopy,
561    ) -> Result<Self::Copier> {
562        let output: GcsCopier = {
563            let copier = GcsCopier::new(self.core.clone(), ctx.clone(), from, to, args);
564            Ok(copier)
565        }?;
566
567        Ok(output)
568    }
569
570    fn compose(&self, ctx: &OperationContext, to: &str, args: OpCompose) -> Result<Self::Composer> {
571        if args.if_match().is_some()
572            || args.if_none_match().is_some()
573            || args.if_version_not_match().is_some()
574        {
575            return Err(Error::new(
576                ErrorKind::Unsupported,
577                "GCS compose does not support the requested destination option",
578            ));
579        }
580        Ok(GcsComposer::new(self.core.clone(), ctx.clone(), to, args))
581    }
582
583    async fn rename(
584        &self,
585        _ctx: &OperationContext,
586        _from: &str,
587        _to: &str,
588        _args: OpRename,
589    ) -> Result<RpRename> {
590        Err(Error::new(
591            ErrorKind::Unsupported,
592            "operation is not supported",
593        ))
594    }
595
596    async fn presign(
597        &self,
598        ctx: &OperationContext,
599        path: &str,
600        args: OpPresign,
601    ) -> Result<RpPresign> {
602        // We will not send this request out, just for signing.
603        let req = match args.operation() {
604            PresignOperation::Stat(v) => self.core.gcs_head_object_xml_request(path, v),
605            PresignOperation::Read(range, v) => {
606                self.core.gcs_get_object_xml_request(path, *range, v)
607            }
608            PresignOperation::Write(v) => {
609                self.core
610                    .gcs_insert_object_xml_request(path, v, Buffer::new())
611            }
612            PresignOperation::Delete(_) => Err(Error::new(
613                ErrorKind::Unsupported,
614                "operation is not supported",
615            )),
616            _ => Err(Error::new(
617                ErrorKind::Unsupported,
618                "operation is not supported",
619            )),
620        };
621        let req = req?;
622        let req = self.core.sign_query(ctx, req, args.expire()).await?;
623
624        // We don't need this request anymore, consume it directly.
625        let (parts, _) = req.into_parts();
626
627        Ok(RpPresign::new(PresignedRequest::new(
628            parts.method,
629            parts.uri,
630            parts.headers,
631        )))
632    }
633}