1use std::fmt::Debug;
19use std::sync::Arc;
20
21use http::StatusCode;
22use http::Uri;
23use log::debug;
24use reqsign_core::Context;
25use reqsign_core::OsEnv;
26use reqsign_core::Signer;
27use reqsign_file_read_tokio::TokioFileRead;
28use reqsign_tencent_cos::DefaultCredentialProvider;
29use reqsign_tencent_cos::RequestSigner;
30use reqsign_tencent_cos::StaticCredentialProvider;
31
32use super::COS_SCHEME;
33use super::config::CosConfig;
34use super::core::parse_error;
35use super::core::*;
36use super::deleter::CosDeleter;
37use super::lister::CosLister;
38use super::lister::CosListers;
39use super::lister::CosObjectVersionsLister;
40use super::reader::*;
41use super::writer::CosWriter;
42use super::writer::CosWriters;
43use opendal_core::raw::*;
44use opendal_core::*;
45
46#[doc = include_str!("docs.md")]
48#[derive(Default)]
49pub struct CosBuilder {
50 pub(super) config: CosConfig,
51}
52
53impl Debug for CosBuilder {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("CosBuilder")
56 .field("config", &self.config)
57 .finish_non_exhaustive()
58 }
59}
60
61impl CosBuilder {
62 pub fn root(mut self, root: &str) -> Self {
66 self.config.root = if root.is_empty() {
67 None
68 } else {
69 Some(root.to_string())
70 };
71
72 self
73 }
74
75 pub fn endpoint(mut self, endpoint: &str) -> Self {
83 if !endpoint.is_empty() {
84 self.config.endpoint = Some(endpoint.trim_end_matches('/').to_string());
85 }
86
87 self
88 }
89
90 pub fn secret_id(mut self, secret_id: &str) -> Self {
94 if !secret_id.is_empty() {
95 self.config.secret_id = Some(secret_id.to_string());
96 }
97
98 self
99 }
100
101 pub fn secret_key(mut self, secret_key: &str) -> Self {
105 if !secret_key.is_empty() {
106 self.config.secret_key = Some(secret_key.to_string());
107 }
108
109 self
110 }
111
112 pub fn security_token(mut self, security_token: &str) -> Self {
128 if !security_token.is_empty() {
129 self.config.security_token = Some(security_token.to_string());
130 }
131
132 self
133 }
134
135 pub fn bucket(mut self, bucket: &str) -> Self {
138 if !bucket.is_empty() {
139 self.config.bucket = Some(bucket.to_string());
140 }
141
142 self
143 }
144
145 #[deprecated(
147 since = "0.57.0",
148 note = "COS versioning capability is enabled by default and this option is no longer needed."
149 )]
150 pub fn enable_versioning(self, _enabled: bool) -> Self {
151 self
152 }
153
154 pub fn disable_config_load(mut self) -> Self {
161 self.config.disable_config_load = true;
162 self
163 }
164}
165
166impl Builder for CosBuilder {
167 type Config = CosConfig;
168
169 fn build(self) -> Result<impl Service> {
170 debug!("backend build started: {:?}", self);
171
172 let root = normalize_root(&self.config.root.unwrap_or_default());
173 debug!("backend use root {root}");
174
175 let bucket = match &self.config.bucket {
176 Some(bucket) => Ok(bucket.to_string()),
177 None => Err(
178 Error::new(ErrorKind::ConfigInvalid, "The bucket is misconfigured")
179 .with_context("service", COS_SCHEME),
180 ),
181 }?;
182 debug!("backend use bucket {}", bucket);
183
184 let uri = match &self.config.endpoint {
185 Some(endpoint) => endpoint.parse::<Uri>().map_err(|err| {
186 Error::new(ErrorKind::ConfigInvalid, "endpoint is invalid")
187 .with_context("service", COS_SCHEME)
188 .with_context("endpoint", endpoint)
189 .set_source(err)
190 }),
191 None => Err(Error::new(ErrorKind::ConfigInvalid, "endpoint is empty")
192 .with_context("service", COS_SCHEME)),
193 }?;
194
195 let endpoint = build_endpoint(&uri, &bucket)?;
196 debug!("backend use endpoint {}", endpoint);
197
198 let os_env = OsEnv;
199 let ctx = Context::new()
200 .with_file_read(TokioFileRead)
201 .with_env(os_env);
202
203 let mut credential = if self.config.disable_config_load {
204 DefaultCredentialProvider::builder()
205 .no_env()
206 .no_web_identity()
207 .build()
208 } else {
209 DefaultCredentialProvider::new()
210 };
211
212 if let (Some(secret_id), Some(secret_key)) = (
213 self.config.secret_id.as_deref(),
214 self.config.secret_key.as_deref(),
215 ) {
216 let static_provider = if let Some(token) = self.config.security_token.as_deref() {
217 StaticCredentialProvider::with_security_token(secret_id, secret_key, token)
218 } else {
219 StaticCredentialProvider::new(secret_id, secret_key)
220 };
221
222 credential = credential.push_front(static_provider);
223 }
224
225 let signer = Signer::new(ctx, credential, RequestSigner::new());
226
227 let info = ServiceInfo::new(COS_SCHEME, &root, &bucket);
228 let capability = Capability {
229 stat: true,
230 stat_with_if_match: true,
231 stat_with_if_none_match: true,
232 stat_with_version: true,
233
234 read: true,
235 read_with_suffix: true,
236
237 read_with_if_match: true,
238 read_with_if_none_match: true,
239 read_with_if_modified_since: true,
240 read_with_if_unmodified_since: true,
241 read_with_version: true,
242
243 write: true,
244 write_can_empty: true,
245 write_can_append: true,
246 write_can_multi: true,
247 write_with_content_type: true,
248 write_with_cache_control: true,
249 write_with_content_disposition: true,
250 write_with_if_not_exists: true,
251 copy_with_if_not_exists: true,
252 write_multi_min_size: Some(1024 * 1024),
256 write_multi_max_size: if cfg!(target_pointer_width = "64") {
260 Some(5 * 1024 * 1024 * 1024)
261 } else {
262 Some(usize::MAX)
263 },
264 write_with_user_metadata: true,
265
266 delete: true,
267 delete_with_version: true,
268 copy: true,
269
270 list: true,
271 list_with_recursive: true,
272 list_with_versions: true,
273 list_with_deleted: true,
274
275 presign: true,
276 presign_stat: true,
277 presign_read: true,
278 presign_write: true,
279
280 shared: true,
281
282 ..Default::default()
283 };
284
285 Ok(CosBackend {
286 core: Arc::new(CosCore {
287 info,
288 capability,
289 bucket: bucket.clone(),
290 root,
291 endpoint,
292 signer,
293 }),
294 })
295 }
296}
297
298#[derive(Debug, Clone)]
300pub struct CosBackend {
301 pub(crate) core: Arc<CosCore>,
302}
303
304impl Service for CosBackend {
305 type Reader = oio::StreamReader<CosReader>;
306 type Writer = CosWriters;
307 type Lister = CosListers;
308 type Deleter = oio::OneShotDeleter<CosDeleter>;
309 type Copier = oio::OneShotCopier;
310 type Composer = ();
311
312 fn info(&self) -> ServiceInfo {
313 self.core.info.clone()
314 }
315
316 fn capability(&self) -> Capability {
317 self.core.capability
318 }
319
320 async fn create_dir(
321 &self,
322 _ctx: &OperationContext,
323 _path: &str,
324 _args: OpCreateDir,
325 ) -> Result<RpCreateDir> {
326 Err(Error::new(
327 ErrorKind::Unsupported,
328 "operation is not supported",
329 ))
330 }
331
332 async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
333 let resp = self.core.cos_head_object(ctx, path, &args).await?;
334
335 let status = resp.status();
336
337 match status {
338 StatusCode::OK => {
339 let headers = resp.headers();
340 let mut meta = parse_into_metadata(path, headers)?.into_builder();
341
342 let user_meta = parse_prefixed_headers(headers, "x-cos-meta-");
343 if !user_meta.is_empty() {
344 meta.user_metadata(user_meta);
345 }
346
347 if let Some(v) = parse_header_to_str(headers, constants::X_COS_VERSION_ID)?
348 && v != "null"
349 {
350 meta.version(v);
351 }
352
353 Ok(RpStat::new(meta.build()))
354 }
355 _ => Err(parse_error(
356 ErrorContext::new(ServiceOperation("HeadObject"))
357 .with_caller_condition(args.is_conditional()),
358 resp,
359 )),
360 }
361 }
362 fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
363 let output: oio::StreamReader<CosReader> = {
364 Ok(oio::StreamReader::new(CosReader::new(
365 self.clone(),
366 ctx.clone(),
367 path,
368 args,
369 )))
370 }?;
371
372 Ok(output)
373 }
374
375 fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
376 let output: CosWriters = {
377 let writer = CosWriter::new(self.core.clone(), ctx.clone(), path, args.clone());
378
379 let w = if args.append() {
380 CosWriters::Two(oio::AppendWriter::new(writer))
381 } else {
382 CosWriters::One(oio::MultipartWriter::new(
383 ctx.executor().clone(),
384 writer,
385 args.concurrent(),
386 ))
387 };
388
389 Ok(w)
390 }?;
391
392 Ok(output)
393 }
394
395 fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
396 let output: oio::OneShotDeleter<CosDeleter> = {
397 Ok(oio::OneShotDeleter::new(CosDeleter::new(
398 self.core.clone(),
399 ctx.clone(),
400 )))
401 }?;
402
403 Ok(output)
404 }
405
406 fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
407 let output: CosListers = {
408 let l = if args.versions() || args.deleted() {
409 TwoWays::Two(oio::PageLister::new(CosObjectVersionsLister::new(
410 self.core.clone(),
411 ctx.clone(),
412 path,
413 args,
414 )))
415 } else {
416 TwoWays::One(oio::PageLister::new(CosLister::new(
417 self.core.clone(),
418 ctx.clone(),
419 path,
420 args.recursive(),
421 args.limit(),
422 )))
423 };
424
425 Ok(l)
426 }?;
427
428 Ok(output)
429 }
430
431 fn copy(
432 &self,
433 ctx: &OperationContext,
434 from: &str,
435 to: &str,
436 args: OpCopy,
437 ) -> Result<Self::Copier> {
438 let core = self.core.clone();
439 let ctx = ctx.clone();
440 let from = from.to_string();
441 let to = to.to_string();
442 Ok(oio::OneShotCopier::new(async move {
443 let source_size = match args.source_content_length_hint() {
444 Some(size) => size,
445 None => {
446 let stat_args = options::StatOptions {
447 version: args.source_version().map(str::to_owned),
448 ..Default::default()
449 }
450 .into();
451 let resp = core.cos_head_object(&ctx, &from, &stat_args).await?;
452 match resp.status() {
453 StatusCode::OK => {
454 parse_into_metadata(&from, resp.headers())?.content_length()
455 }
456 _ => {
457 return Err(parse_error(
458 ErrorContext::new(ServiceOperation("HeadObject")),
459 resp,
460 ));
461 }
462 }
463 }
464 };
465 let resp = core.cos_copy_object(&ctx, &from, &to, &args).await?;
466
467 let status = resp.status();
468
469 match status {
470 StatusCode::OK => Ok(MetadataBuilder::file(source_size).build()),
471 _ => Err(parse_error(
472 ErrorContext::new(ServiceOperation("CopyObject"))
473 .with_if_not_exists(args.if_not_exists()),
474 resp,
475 )),
476 }
477 }))
478 }
479
480 async fn rename(
481 &self,
482 _ctx: &OperationContext,
483 _from: &str,
484 _to: &str,
485 _args: OpRename,
486 ) -> Result<RpRename> {
487 Err(Error::new(
488 ErrorKind::Unsupported,
489 "operation is not supported",
490 ))
491 }
492
493 async fn presign(
494 &self,
495 ctx: &OperationContext,
496 path: &str,
497 args: OpPresign,
498 ) -> Result<RpPresign> {
499 let req = match args.operation() {
500 PresignOperation::Stat(v) => self.core.cos_head_object_request(path, v),
501 PresignOperation::Read(range, v) => self.core.cos_get_object_request(path, *range, v),
502 PresignOperation::Write(v) => {
503 self.core
504 .cos_put_object_request(path, None, v, Buffer::new())
505 }
506 PresignOperation::Delete(_) => Err(Error::new(
507 ErrorKind::Unsupported,
508 "operation is not supported",
509 )),
510 _ => Err(Error::new(
511 ErrorKind::Unsupported,
512 "operation is not supported",
513 )),
514 };
515 let req = req?;
516 let req = self.core.sign_query(ctx, req, args.expire()).await?;
517
518 let (parts, _) = req.into_parts();
520
521 Ok(RpPresign::new(PresignedRequest::new(
522 parts.method,
523 parts.uri,
524 parts.headers,
525 )))
526 }
527}
528
529fn build_endpoint(uri: &Uri, bucket: &str) -> Result<String> {
533 let scheme = uri.scheme_str().unwrap_or("https");
534
535 let host = uri.host().ok_or_else(|| {
536 Error::new(ErrorKind::ConfigInvalid, "endpoint host is empty")
537 .with_context("service", COS_SCHEME)
538 .with_context("endpoint", uri.to_string())
539 })?;
540
541 let host = host.strip_prefix(&format!("{bucket}.")).unwrap_or(host);
543
544 Ok(match uri.port_u16() {
547 Some(port) => format!("{scheme}://{bucket}.{host}:{port}"),
548 None => format!("{scheme}://{bucket}.{host}"),
549 })
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 fn endpoint_of(raw: &str, bucket: &str) -> String {
557 build_endpoint(&raw.parse::<Uri>().unwrap(), bucket).unwrap()
558 }
559
560 #[test]
561 fn build_endpoint_keeps_a_custom_port() {
562 assert_eq!(
563 endpoint_of(
564 "https://cos.internal.example.com:8443",
565 "examplebucket-1250000000"
566 ),
567 "https://examplebucket-1250000000.cos.internal.example.com:8443"
568 );
569 }
570
571 #[test]
572 fn build_endpoint_prefixes_the_bucket() {
573 assert_eq!(
574 endpoint_of(
575 "https://cos.ap-guangzhou.myqcloud.com",
576 "examplebucket-1250000000"
577 ),
578 "https://examplebucket-1250000000.cos.ap-guangzhou.myqcloud.com"
579 );
580 }
581
582 #[test]
583 fn build_endpoint_does_not_repeat_a_bucket_already_in_the_host() {
584 assert_eq!(
587 endpoint_of(
588 "https://examplebucket-1250000000.cos.ap-guangzhou.myqcloud.com",
589 "examplebucket-1250000000"
590 ),
591 "https://examplebucket-1250000000.cos.ap-guangzhou.myqcloud.com"
592 );
593 }
594
595 #[test]
596 fn build_endpoint_defaults_the_scheme_to_https() {
597 assert_eq!(
600 endpoint_of("cos.ap-guangzhou.myqcloud.com", "b"),
601 "https://b.cos.ap-guangzhou.myqcloud.com"
602 );
603 }
604
605 #[test]
606 fn build_endpoint_reports_a_missing_host_instead_of_panicking() {
607 for raw in ["/just/a/path", "//cos.ap-guangzhou.myqcloud.com"] {
609 let uri = raw.parse::<Uri>().unwrap();
610 assert!(
611 build_endpoint(&uri, "b").is_err(),
612 "expected an error for {raw}"
613 );
614 }
615 }
616}