Skip to main content

opendal_service_b2/
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 asyncband::rwlock::RwLock;
22use http::Request;
23use http::StatusCode;
24use log::debug;
25
26use super::B2_SCHEME;
27use super::config::B2Config;
28use super::core::B2Signer;
29use super::core::constants;
30use super::core::parse_error;
31use super::core::parse_file_info;
32use super::core::{B2Core, ErrorContext};
33use super::deleter::B2Deleter;
34use super::lister::B2Lister;
35use super::reader::*;
36use super::writer::B2Writer;
37use super::writer::B2Writers;
38use opendal_core::raw::*;
39use opendal_core::*;
40
41/// [b2](https://www.backblaze.com/cloud-storage) services support.
42#[doc = include_str!("docs.md")]
43#[derive(Default)]
44pub struct B2Builder {
45    pub(super) config: B2Config,
46}
47
48impl Debug for B2Builder {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("B2Builder")
51            .field("config", &self.config)
52            .finish_non_exhaustive()
53    }
54}
55
56impl B2Builder {
57    /// Set root of this backend.
58    ///
59    /// All operations will happen under this root.
60    pub fn root(mut self, root: &str) -> Self {
61        self.config.root = if root.is_empty() {
62            None
63        } else {
64            Some(root.to_string())
65        };
66
67        self
68    }
69
70    /// application_key_id of this backend.
71    pub fn application_key_id(mut self, application_key_id: &str) -> Self {
72        self.config.application_key_id = if application_key_id.is_empty() {
73            None
74        } else {
75            Some(application_key_id.to_string())
76        };
77
78        self
79    }
80
81    /// application_key of this backend.
82    pub fn application_key(mut self, application_key: &str) -> Self {
83        self.config.application_key = if application_key.is_empty() {
84            None
85        } else {
86            Some(application_key.to_string())
87        };
88
89        self
90    }
91
92    /// Set bucket name of this backend.
93    /// You can find it in <https://secure.backblaze.com/b2_buckets.html>
94    pub fn bucket(mut self, bucket: &str) -> Self {
95        self.config.bucket = bucket.to_string();
96
97        self
98    }
99
100    /// Set bucket id of this backend.
101    /// You can find it in <https://secure.backblaze.com/b2_buckets.html>
102    pub fn bucket_id(mut self, bucket_id: &str) -> Self {
103        self.config.bucket_id = bucket_id.to_string();
104
105        self
106    }
107}
108
109impl Builder for B2Builder {
110    type Config = B2Config;
111
112    /// Builds the backend and returns the result of B2Backend.
113    fn build(self) -> Result<impl Service> {
114        debug!("backend build started: {:?}", self);
115
116        let root = normalize_root(&self.config.root.clone().unwrap_or_default());
117        debug!("backend use root {}", root);
118
119        // Handle bucket.
120        if self.config.bucket.is_empty() {
121            return Err(Error::new(ErrorKind::ConfigInvalid, "bucket is empty")
122                .with_operation("Builder::build")
123                .with_context("service", B2_SCHEME));
124        }
125
126        debug!("backend use bucket {}", self.config.bucket);
127
128        // Handle bucket_id.
129        if self.config.bucket_id.is_empty() {
130            return Err(Error::new(ErrorKind::ConfigInvalid, "bucket_id is empty")
131                .with_operation("Builder::build")
132                .with_context("service", B2_SCHEME));
133        }
134
135        debug!("backend bucket_id {}", self.config.bucket_id);
136
137        let application_key_id = match &self.config.application_key_id {
138            Some(application_key_id) => Ok(application_key_id.clone()),
139            None => Err(
140                Error::new(ErrorKind::ConfigInvalid, "application_key_id is empty")
141                    .with_operation("Builder::build")
142                    .with_context("service", B2_SCHEME),
143            ),
144        }?;
145
146        let application_key = match &self.config.application_key {
147            Some(key_id) => Ok(key_id.clone()),
148            None => Err(
149                Error::new(ErrorKind::ConfigInvalid, "application_key is empty")
150                    .with_operation("Builder::build")
151                    .with_context("service", B2_SCHEME),
152            ),
153        }?;
154
155        let signer = B2Signer {
156            application_key_id,
157            application_key,
158            ..Default::default()
159        };
160
161        Ok(B2Backend {
162            core: Arc::new(B2Core {
163                info: ServiceInfo::new(B2_SCHEME, &root, ""),
164                capability: Capability {
165                    stat: true,
166
167                    read: true,
168                    read_with_suffix: true,
169
170                    write: true,
171                    write_can_empty: true,
172                    write_can_multi: true,
173                    write_with_content_type: true,
174                    write_with_user_metadata: true,
175                    // The min multipart size of b2 is 5 MiB.
176                    //
177                    // ref: <https://www.backblaze.com/docs/cloud-storage-large-files>
178                    write_multi_min_size: Some(5 * 1024 * 1024),
179                    // The max multipart size of b2 is 5 Gb.
180                    //
181                    // ref: <https://www.backblaze.com/docs/cloud-storage-large-files>
182                    write_multi_max_size: if cfg!(target_pointer_width = "64") {
183                        Some(5 * 1024 * 1024 * 1024)
184                    } else {
185                        Some(usize::MAX)
186                    },
187
188                    delete: true,
189                    copy: true,
190
191                    list: true,
192                    list_with_limit: true,
193                    list_with_start_after: true,
194                    list_with_recursive: true,
195
196                    presign: true,
197                    presign_read: true,
198                    presign_write: true,
199                    presign_stat: true,
200
201                    shared: true,
202
203                    ..Default::default()
204                },
205                signer: Arc::new(RwLock::new(signer)),
206                root,
207
208                bucket: self.config.bucket.clone(),
209                bucket_id: self.config.bucket_id.clone(),
210            }),
211        })
212    }
213}
214
215/// Backend for b2 services.
216#[derive(Debug, Clone)]
217pub struct B2Backend {
218    pub(crate) core: Arc<B2Core>,
219}
220
221impl Service for B2Backend {
222    type Reader = oio::StreamReader<B2Reader>;
223    type Writer = B2Writers;
224    type Lister = oio::PageLister<B2Lister>;
225    type Deleter = oio::OneShotDeleter<B2Deleter>;
226    type Copier = oio::OneShotCopier;
227    type Composer = ();
228
229    fn info(&self) -> ServiceInfo {
230        self.core.info.clone()
231    }
232
233    fn capability(&self) -> Capability {
234        self.core.capability
235    }
236
237    async fn create_dir(
238        &self,
239        _ctx: &OperationContext,
240        _path: &str,
241        _args: OpCreateDir,
242    ) -> Result<RpCreateDir> {
243        Err(Error::new(
244            ErrorKind::Unsupported,
245            "operation is not supported",
246        ))
247    }
248
249    /// B2 have a get_file_info api required a file_id field, but field_id need call list api, list api also return file info
250    /// So we call list api to get file info
251    async fn stat(&self, ctx: &OperationContext, path: &str, _args: OpStat) -> Result<RpStat> {
252        // Stat root always returns a DIR.
253        if path == "/" {
254            return Ok(RpStat::new(MetadataBuilder::dir().build()));
255        }
256
257        let delimiter = if path.ends_with('/') { Some("/") } else { None };
258
259        let file_info = self.core.get_file_info(ctx, path, delimiter).await?;
260        let meta = parse_file_info(&file_info);
261        Ok(RpStat::new(meta))
262    }
263    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
264        let output: oio::StreamReader<B2Reader> = {
265            Ok(oio::StreamReader::new(B2Reader::new(
266                self.clone(),
267                ctx.clone(),
268                path,
269                args,
270            )))
271        }?;
272
273        Ok(output)
274    }
275
276    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
277        let output: B2Writers = {
278            let concurrent = args.concurrent();
279            let writer = B2Writer::new(self.core.clone(), ctx.clone(), path, args);
280
281            let w = oio::MultipartWriter::new(ctx.executor().clone(), writer, concurrent);
282
283            Ok(w)
284        }?;
285
286        Ok(output)
287    }
288
289    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
290        let output: oio::OneShotDeleter<B2Deleter> = {
291            Ok(oio::OneShotDeleter::new(B2Deleter::new(
292                self.core.clone(),
293                ctx.clone(),
294            )))
295        }?;
296
297        Ok(output)
298    }
299
300    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
301        let output: oio::PageLister<B2Lister> = {
302            Ok(oio::PageLister::new(B2Lister::new(
303                self.core.clone(),
304                ctx.clone(),
305                path,
306                args.recursive(),
307                args.limit(),
308                args.start_after(),
309            )))
310        }?;
311
312        Ok(output)
313    }
314
315    fn copy(
316        &self,
317        ctx: &OperationContext,
318        from: &str,
319        to: &str,
320        _args: OpCopy,
321    ) -> Result<Self::Copier> {
322        let core = self.core.clone();
323        let ctx = ctx.clone();
324        let from = from.to_string();
325        let to = to.to_string();
326
327        Ok(oio::OneShotCopier::new(async move {
328            let file_info = core.get_file_info(&ctx, &from, None).await?;
329            let source_file_id = file_info.file_id;
330            let source_content_length = file_info.content_length;
331
332            let Some(source_file_id) = source_file_id else {
333                return Err(Error::new(ErrorKind::IsADirectory, "is a directory"));
334            };
335
336            let resp = core.copy_file(&ctx, source_file_id, &to).await?;
337
338            let status = resp.status();
339
340            match status {
341                StatusCode::OK => {
342                    let metadata = MetadataBuilder::file(source_content_length);
343                    Ok(metadata.build())
344                }
345                _ => Err(parse_error(
346                    ErrorContext::new(ServiceOperation("CopyFile")),
347                    resp,
348                )),
349            }
350        }))
351    }
352
353    async fn rename(
354        &self,
355        _ctx: &OperationContext,
356        _from: &str,
357        _to: &str,
358        _args: OpRename,
359    ) -> Result<RpRename> {
360        Err(Error::new(
361            ErrorKind::Unsupported,
362            "operation is not supported",
363        ))
364    }
365
366    async fn presign(
367        &self,
368        ctx: &OperationContext,
369        path: &str,
370        args: OpPresign,
371    ) -> Result<RpPresign> {
372        match args.operation() {
373            PresignOperation::Stat(_) => {
374                let resp = self
375                    .core
376                    .get_download_authorization(ctx, path, args.expire())
377                    .await?;
378                let path = build_abs_path(&self.core.root, path);
379
380                let auth_info = self.core.get_auth_info(ctx).await?;
381
382                let url = format!(
383                    "{}/file/{}/{}?Authorization={}",
384                    auth_info.download_url,
385                    self.core.bucket,
386                    percent_encode_path(&path),
387                    resp.authorization_token
388                );
389
390                let req = Request::get(url);
391
392                let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
393
394                // We don't need this request anymore, consume
395                let (parts, _) = req.into_parts();
396
397                Ok(RpPresign::new(PresignedRequest::new(
398                    parts.method,
399                    parts.uri,
400                    parts.headers,
401                )))
402            }
403            PresignOperation::Read(range, _) => {
404                let resp = self
405                    .core
406                    .get_download_authorization(ctx, path, args.expire())
407                    .await?;
408                let path = build_abs_path(&self.core.root, path);
409
410                let auth_info = self.core.get_auth_info(ctx).await?;
411
412                let url = format!(
413                    "{}/file/{}/{}?Authorization={}",
414                    auth_info.download_url,
415                    self.core.bucket,
416                    percent_encode_path(&path),
417                    resp.authorization_token
418                );
419
420                let mut req = Request::get(url);
421                if !range.is_full() {
422                    req = req.header(http::header::RANGE, range.to_header());
423                }
424
425                let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
426
427                // We don't need this request anymore, consume
428                let (parts, _) = req.into_parts();
429
430                Ok(RpPresign::new(PresignedRequest::new(
431                    parts.method,
432                    parts.uri,
433                    parts.headers,
434                )))
435            }
436            PresignOperation::Write(_) => {
437                let resp = self.core.get_upload_url(ctx).await?;
438
439                let mut req = Request::post(&resp.upload_url);
440
441                req = req.header(http::header::AUTHORIZATION, resp.authorization_token);
442                req = req.header(
443                    "X-Bz-File-Name",
444                    percent_encode_path(&build_abs_path(&self.core.root, path)),
445                );
446                req = req.header(http::header::CONTENT_TYPE, "b2/x-auto");
447                req = req.header(constants::X_BZ_CONTENT_SHA1, "do_not_verify");
448
449                let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
450                // We don't need this request anymore, consume it directly.
451                let (parts, _) = req.into_parts();
452
453                Ok(RpPresign::new(PresignedRequest::new(
454                    parts.method,
455                    parts.uri,
456                    parts.headers,
457                )))
458            }
459            PresignOperation::Delete(_) => Err(Error::new(
460                ErrorKind::Unsupported,
461                "operation is not supported",
462            )),
463            _ => Err(Error::new(
464                ErrorKind::Unsupported,
465                "operation is not supported",
466            )),
467        }
468    }
469}