1use std::fmt::Debug;
19use std::sync::Arc;
20
21use http::Request;
22use http::StatusCode;
23use log::debug;
24use mea::rwlock::RwLock;
25
26use super::B2_SCHEME;
27use super::config::B2Config;
28use super::core::B2Core;
29use super::core::B2Signer;
30use super::core::constants;
31use super::core::parse_error;
32use super::core::parse_file_info;
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#[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 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 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 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 pub fn bucket(mut self, bucket: &str) -> Self {
95 self.config.bucket = bucket.to_string();
96
97 self
98 }
99
100 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 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 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 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 write_multi_min_size: Some(5 * 1024 * 1024),
179 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#[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
228 fn info(&self) -> ServiceInfo {
229 self.core.info.clone()
230 }
231
232 fn capability(&self) -> Capability {
233 self.core.capability
234 }
235
236 async fn create_dir(
237 &self,
238 _ctx: &OperationContext,
239 _path: &str,
240 _args: OpCreateDir,
241 ) -> Result<RpCreateDir> {
242 Err(Error::new(
243 ErrorKind::Unsupported,
244 "operation is not supported",
245 ))
246 }
247
248 async fn stat(&self, ctx: &OperationContext, path: &str, _args: OpStat) -> Result<RpStat> {
251 if path == "/" {
253 return Ok(RpStat::new(Metadata::new(EntryMode::DIR)));
254 }
255
256 let delimiter = if path.ends_with('/') { Some("/") } else { None };
257
258 let file_info = self.core.get_file_info(ctx, path, delimiter).await?;
259 let meta = parse_file_info(&file_info);
260 Ok(RpStat::new(meta))
261 }
262 fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
263 let output: oio::StreamReader<B2Reader> = {
264 Ok(oio::StreamReader::new(B2Reader::new(
265 self.clone(),
266 ctx.clone(),
267 path,
268 args,
269 )))
270 }?;
271
272 Ok(output)
273 }
274
275 fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
276 let output: B2Writers = {
277 let concurrent = args.concurrent();
278 let writer = B2Writer::new(self.core.clone(), ctx.clone(), path, args);
279
280 let w = oio::MultipartWriter::new(ctx.executor().clone(), writer, concurrent);
281
282 Ok(w)
283 }?;
284
285 Ok(output)
286 }
287
288 fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
289 let output: oio::OneShotDeleter<B2Deleter> = {
290 Ok(oio::OneShotDeleter::new(B2Deleter::new(
291 self.core.clone(),
292 ctx.clone(),
293 )))
294 }?;
295
296 Ok(output)
297 }
298
299 fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
300 let output: oio::PageLister<B2Lister> = {
301 Ok(oio::PageLister::new(B2Lister::new(
302 self.core.clone(),
303 ctx.clone(),
304 path,
305 args.recursive(),
306 args.limit(),
307 args.start_after(),
308 )))
309 }?;
310
311 Ok(output)
312 }
313
314 fn copy(
315 &self,
316 ctx: &OperationContext,
317 from: &str,
318 to: &str,
319 _args: OpCopy,
320 _opts: OpCopier,
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
331 let Some(source_file_id) = source_file_id else {
332 return Err(Error::new(ErrorKind::IsADirectory, "is a directory"));
333 };
334
335 let resp = core.copy_file(&ctx, source_file_id, &to).await?;
336
337 let status = resp.status();
338
339 match status {
340 StatusCode::OK => Ok(Metadata::default()),
341 _ => Err(parse_error(resp)),
342 }
343 }))
344 }
345
346 async fn rename(
347 &self,
348 _ctx: &OperationContext,
349 _from: &str,
350 _to: &str,
351 _args: OpRename,
352 ) -> Result<RpRename> {
353 Err(Error::new(
354 ErrorKind::Unsupported,
355 "operation is not supported",
356 ))
357 }
358
359 async fn presign(
360 &self,
361 ctx: &OperationContext,
362 path: &str,
363 args: OpPresign,
364 ) -> Result<RpPresign> {
365 match args.operation() {
366 PresignOperation::Stat(_) => {
367 let resp = self
368 .core
369 .get_download_authorization(ctx, path, args.expire())
370 .await?;
371 let path = build_abs_path(&self.core.root, path);
372
373 let auth_info = self.core.get_auth_info(ctx).await?;
374
375 let url = format!(
376 "{}/file/{}/{}?Authorization={}",
377 auth_info.download_url, self.core.bucket, path, resp.authorization_token
378 );
379
380 let req = Request::get(url);
381
382 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
383
384 let (parts, _) = req.into_parts();
386
387 Ok(RpPresign::new(PresignedRequest::new(
388 parts.method,
389 parts.uri,
390 parts.headers,
391 )))
392 }
393 PresignOperation::Read(_, _) => {
394 let resp = self
395 .core
396 .get_download_authorization(ctx, path, args.expire())
397 .await?;
398 let path = build_abs_path(&self.core.root, path);
399
400 let auth_info = self.core.get_auth_info(ctx).await?;
401
402 let url = format!(
403 "{}/file/{}/{}?Authorization={}",
404 auth_info.download_url, self.core.bucket, path, resp.authorization_token
405 );
406
407 let req = Request::get(url);
408
409 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
410
411 let (parts, _) = req.into_parts();
413
414 Ok(RpPresign::new(PresignedRequest::new(
415 parts.method,
416 parts.uri,
417 parts.headers,
418 )))
419 }
420 PresignOperation::Write(_) => {
421 let resp = self.core.get_upload_url(ctx).await?;
422
423 let mut req = Request::post(&resp.upload_url);
424
425 req = req.header(http::header::AUTHORIZATION, resp.authorization_token);
426 req = req.header("X-Bz-File-Name", build_abs_path(&self.core.root, path));
427 req = req.header(http::header::CONTENT_TYPE, "b2/x-auto");
428 req = req.header(constants::X_BZ_CONTENT_SHA1, "do_not_verify");
429
430 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
431 let (parts, _) = req.into_parts();
433
434 Ok(RpPresign::new(PresignedRequest::new(
435 parts.method,
436 parts.uri,
437 parts.headers,
438 )))
439 }
440 PresignOperation::Delete(_) => Err(Error::new(
441 ErrorKind::Unsupported,
442 "operation is not supported",
443 )),
444 _ => Err(Error::new(
445 ErrorKind::Unsupported,
446 "operation is not supported",
447 )),
448 }
449 }
450}