1use std::fmt::Debug;
19use std::sync::Arc;
20
21use log::debug;
22
23use super::GOOSEFS_SCHEME;
24use super::config::GoosefsConfig;
25use super::core::GoosefsCore;
26use super::deleter::GoosefsDeleter;
27use super::lister::GoosefsLister;
28use super::reader::*;
29use super::writer::GoosefsWriter;
30use super::writer::GoosefsWriters;
31use opendal_core::raw::*;
32use opendal_core::*;
33
34#[doc = include_str!("docs.md")]
36#[derive(Default)]
37pub struct GoosefsBuilder {
38 pub(super) config: GoosefsConfig,
39}
40
41impl Debug for GoosefsBuilder {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_struct("GoosefsBuilder")
44 .field("config", &self.config)
45 .finish_non_exhaustive()
46 }
47}
48
49impl GoosefsBuilder {
50 pub fn root(mut self, root: &str) -> Self {
54 self.config.root = if root.is_empty() {
55 None
56 } else {
57 Some(root.to_string())
58 };
59 self
60 }
61
62 pub fn master_addr(mut self, addr: &str) -> Self {
73 if !addr.is_empty() {
74 self.config.master_addr = Some(addr.to_string());
75 }
76 self
77 }
78
79 pub fn block_size(mut self, size: u64) -> Self {
81 self.config.block_size = Some(size);
82 self
83 }
84
85 pub fn chunk_size(mut self, size: u64) -> Self {
87 self.config.chunk_size = Some(size);
88 self
89 }
90
91 pub fn write_type(mut self, wt: &str) -> Self {
97 if !wt.is_empty() {
98 self.config.write_type = Some(wt.to_string());
99 }
100 self
101 }
102
103 pub fn auth_type(mut self, auth_type: &str) -> Self {
109 if !auth_type.is_empty() {
110 self.config.auth_type = Some(auth_type.to_string());
111 }
112 self
113 }
114
115 pub fn auth_username(mut self, username: &str) -> Self {
120 if !username.is_empty() {
121 self.config.auth_username = Some(username.to_string());
122 }
123 self
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136enum MasterAddrSource {
137 Env,
139 SiteProperties,
142 Unset,
144}
145
146fn apply_master_addr_precedence(
163 config: &mut goosefs_sdk::config::GoosefsConfig,
164 explicit: Option<&str>,
165) -> Result<()> {
166 let explicit_addrs = match explicit {
167 Some(master_addr) => {
168 let addrs: Vec<String> = master_addr
169 .split(',')
170 .map(|s| s.trim().to_string())
171 .filter(|s| !s.is_empty())
172 .collect();
173 if addrs.is_empty() {
174 return Err(Error::new(
175 ErrorKind::ConfigInvalid,
176 "master_addr is empty after trimming",
177 )
178 .with_operation("Builder::build")
179 .with_context("service", GOOSEFS_SCHEME));
180 }
181 Some(addrs)
182 }
183 None => None,
184 };
185
186 match detect_master_addr_source() {
187 MasterAddrSource::Unset => {
188 let Some(addrs) = explicit_addrs else {
189 return Err(Error::new(
190 ErrorKind::ConfigInvalid,
191 "master_addr is not configured: set it via GoosefsBuilder::master_addr(...), \
192 the `master_addr` config key, the GOOSEFS_MASTER_ADDR env var, \
193 or `goosefs.master.hostname`/`goosefs.master.rpc.addresses` in goosefs-site.properties",
194 )
195 .with_operation("Builder::build")
196 .with_context("service", GOOSEFS_SCHEME));
197 };
198
199 config.master_addr = addrs[0].clone();
200 config.master_addrs = if addrs.len() > 1 { addrs } else { Vec::new() };
201 }
202 source => {
203 if let Some(addrs) = explicit_addrs {
204 debug!(
205 "GoosefsBuilder ignores master_addr {addrs:?}: {source:?} supplies {} (addrs={:?})",
206 config.master_addr, config.master_addrs
207 );
208 }
209 }
210 }
211
212 Ok(())
213}
214
215fn detect_master_addr_source() -> MasterAddrSource {
218 let probe = goosefs_sdk::config::GoosefsConfig {
224 master_addr: String::new(),
225 master_addrs: Vec::new(),
226 ..Default::default()
227 }
228 .apply_env();
229 if !probe.master_addr.is_empty() || !probe.master_addrs.is_empty() {
230 return MasterAddrSource::Env;
231 }
232
233 if let Some(path) = goosefs_sdk::config::discover_config_file()
238 && let Ok(content) = std::fs::read_to_string(&path)
239 && site_properties_declare_master(&content)
240 {
241 return MasterAddrSource::SiteProperties;
242 }
243
244 MasterAddrSource::Unset
245}
246
247fn site_properties_declare_master(content: &str) -> bool {
256 let mut addresses = None;
257 let mut hostname = None;
258
259 for line in content.lines() {
260 let line = line.trim();
261 if line.is_empty() || line.starts_with('#') || line.starts_with('!') {
262 continue;
263 }
264 let Some(sep) = line.find('=').or_else(|| line.find(':')) else {
265 continue;
266 };
267
268 match line[..sep].trim() {
269 "goosefs.master.rpc.addresses" => addresses = Some(line[sep + 1..].trim()),
270 "goosefs.master.hostname" => hostname = Some(line[sep + 1..].trim()),
271 _ => {}
272 }
273 }
274
275 match addresses {
276 Some(addresses) => addresses.split(',').any(|addr| !addr.trim().is_empty()),
277 None => hostname.is_some_and(|hostname| !hostname.is_empty()),
278 }
279}
280
281impl Builder for GoosefsBuilder {
282 type Config = GoosefsConfig;
283
284 fn build(self) -> Result<impl Service> {
286 debug!("GoosefsBuilder::build started: {:?}", self);
287
288 let root = normalize_root(&self.config.root.clone().unwrap_or_default());
289 debug!("GoosefsBuilder use root {}", root);
290
291 let mut goosefs_config = goosefs_sdk::config::GoosefsConfig::from_properties_auto()
317 .map_err(|e| {
318 Error::new(
319 ErrorKind::ConfigInvalid,
320 format!("failed to auto-load goosefs config: {e}"),
321 )
322 .with_operation("Builder::build")
323 .with_context("service", GOOSEFS_SCHEME)
324 })?;
325
326 goosefs_config.root = root.clone();
328
329 apply_master_addr_precedence(&mut goosefs_config, self.config.master_addr.as_deref())?;
331 debug!(
332 "GoosefsBuilder use master_addr {} (addrs={:?})",
333 goosefs_config.master_addr, goosefs_config.master_addrs
334 );
335
336 if let Some(block_size) = self.config.block_size {
339 goosefs_config.block_size = block_size;
340 }
341 if let Some(chunk_size) = self.config.chunk_size {
342 goosefs_config.chunk_size = chunk_size;
343 }
344
345 if let Some(ref wt) = self.config.write_type {
349 goosefs_config = goosefs_config.with_write_type_str(wt).map_err(|e| {
350 Error::new(
351 ErrorKind::ConfigInvalid,
352 format!("invalid write_type: {}", e),
353 )
354 .with_operation("Builder::build")
355 .with_context("service", GOOSEFS_SCHEME)
356 })?;
357 }
358
359 if let Some(ref auth_type_str) = self.config.auth_type {
361 goosefs_config = goosefs_config
362 .with_auth_type_str(auth_type_str)
363 .map_err(|e| {
364 Error::new(
365 ErrorKind::ConfigInvalid,
366 format!("invalid auth_type: {}", e),
367 )
368 .with_operation("Builder::build")
369 .with_context("service", GOOSEFS_SCHEME)
370 })?;
371 }
372
373 if let Some(ref auth_username) = self.config.auth_username {
374 goosefs_config = goosefs_config.with_auth_username(auth_username);
375 }
376
377 goosefs_config.validate().map_err(|e| {
379 Error::new(
380 ErrorKind::ConfigInvalid,
381 format!("invalid goosefs config: {e}"),
382 )
383 .with_operation("Builder::build")
384 .with_context("service", GOOSEFS_SCHEME)
385 })?;
386
387 Ok(GoosefsBackend {
388 core: Arc::new(GoosefsCore::new(
389 ServiceInfo::new(GOOSEFS_SCHEME, &root, ""),
390 Capability {
391 stat: true,
392 read: true,
393 write: true,
394 write_can_multi: true,
395 write_with_if_not_exists: true,
400 create_dir: true,
401 delete: true,
402 list: true,
403 rename: true,
404 rename_with_if_not_exists: true,
405 shared: true,
406 ..Default::default()
407 },
408 root,
409 goosefs_config,
410 )),
411 })
412 }
413}
414
415#[derive(Debug, Clone)]
416pub struct GoosefsBackend {
417 pub(crate) core: Arc<GoosefsCore>,
418}
419
420impl Service for GoosefsBackend {
421 type Reader = oio::StreamReader<GoosefsReader>;
422 type Writer = GoosefsWriters;
423 type Lister = oio::PageLister<GoosefsLister>;
424 type Deleter = oio::OneShotDeleter<GoosefsDeleter>;
425 type Copier = ();
426 type Composer = ();
427
428 fn info(&self) -> ServiceInfo {
429 self.core.info.clone()
430 }
431
432 fn capability(&self) -> Capability {
433 self.core.capability
434 }
435
436 async fn create_dir(
437 &self,
438 _ctx: &OperationContext,
439 path: &str,
440 _: OpCreateDir,
441 ) -> Result<RpCreateDir> {
442 self.core.create_dir(path).await?;
443 Ok(RpCreateDir::default())
444 }
445
446 async fn stat(&self, _ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
447 let file_info = self.core.get_status(path).await?;
448 Ok(RpStat::new(self.core.file_info_to_metadata(&file_info)?))
449 }
450 fn read(&self, _ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
451 let output: oio::StreamReader<GoosefsReader> = {
452 Ok(oio::StreamReader::new(GoosefsReader::new(
453 self.clone(),
454 path,
455 args,
456 )))
457 }?;
458
459 Ok(output)
460 }
461
462 fn write(&self, _ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
463 let output: GoosefsWriters = {
464 let w = GoosefsWriter::new(self.core.clone(), args.clone(), path.to_string());
465 Ok(w)
466 }?;
467
468 Ok(output)
469 }
470
471 fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
472 let output: oio::OneShotDeleter<GoosefsDeleter> = {
473 Ok(oio::OneShotDeleter::new(GoosefsDeleter::new(
474 self.core.clone(),
475 )))
476 }?;
477
478 Ok(output)
479 }
480
481 fn list(&self, _ctx: &OperationContext, path: &str, _args: OpList) -> Result<Self::Lister> {
482 let output: oio::PageLister<GoosefsLister> = {
483 let l = GoosefsLister::new(self.core.clone(), path);
484 Ok(oio::PageLister::new(l))
485 }?;
486
487 Ok(output)
488 }
489
490 fn copy(
491 &self,
492 _ctx: &OperationContext,
493 _from: &str,
494 _to: &str,
495 _args: OpCopy,
496 ) -> Result<Self::Copier> {
497 Err(Error::new(
498 ErrorKind::Unsupported,
499 "operation is not supported",
500 ))
501 }
502
503 async fn rename(
504 &self,
505 _ctx: &OperationContext,
506 from: &str,
507 to: &str,
508 args: OpRename,
509 ) -> Result<RpRename> {
510 self.core.rename(from, to, args.if_not_exists()).await?;
511 Ok(RpRename::default())
512 }
513
514 async fn presign(
515 &self,
516 _ctx: &OperationContext,
517 _path: &str,
518 _args: OpPresign,
519 ) -> Result<RpPresign> {
520 Err(Error::new(
521 ErrorKind::Unsupported,
522 "operation is not supported",
523 ))
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 use goosefs_sdk::config::ENV_CONF_DIR;
531 use goosefs_sdk::config::ENV_CONFIG_FILE;
532 use goosefs_sdk::config::ENV_HOME;
533 use goosefs_sdk::config::ENV_MASTER_ADDR;
534 use std::path::PathBuf;
535 use std::sync::Mutex;
536
537 static ENV_LOCK: Mutex<()> = Mutex::new(());
540
541 const SITE_HA: &str =
542 "goosefs.master.rpc.addresses=172.31.5.10:9200,172.31.5.2:9200,172.31.5.11:9200\n";
543 const SITE_NO_MASTER: &str = "goosefs.user.block.size.bytes.default=4MB\n";
544
545 fn site_properties(name: &str, content: &str) -> PathBuf {
552 let path = std::env::temp_dir().join(format!(
553 "opendal_goosefs_{}_{name}_site.properties",
554 std::process::id()
555 ));
556 std::fs::write(&path, content).expect("write goosefs-site.properties");
557
558 unsafe {
559 std::env::set_var(ENV_CONFIG_FILE, &path);
560 std::env::remove_var(ENV_CONF_DIR);
561 std::env::remove_var(ENV_HOME);
562 std::env::remove_var(ENV_MASTER_ADDR);
563 }
564
565 path
566 }
567
568 fn clear_site_properties(path: &PathBuf) {
569 unsafe {
570 std::env::remove_var(ENV_CONFIG_FILE);
571 std::env::remove_var(ENV_MASTER_ADDR);
572 }
573 let _ = std::fs::remove_file(path);
574 }
575
576 fn resolve(explicit: Option<&str>) -> Result<(String, Vec<String>)> {
579 let mut config = goosefs_sdk::config::GoosefsConfig::from_properties_auto()
580 .expect("auto-load must succeed");
581 apply_master_addr_precedence(&mut config, explicit)?;
582 Ok((config.master_addr, config.master_addrs))
583 }
584
585 #[test]
592 fn site_properties_outrank_uri_authority() {
593 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
594 let path = site_properties("site_over_uri", SITE_HA);
595
596 let (master_addr, master_addrs) =
597 resolve(Some("192.0.2.5:9999")).expect("resolution must succeed");
598
599 assert_eq!(master_addr, "172.31.5.10:9200");
600 assert_eq!(
601 master_addrs,
602 vec![
603 "172.31.5.10:9200".to_string(),
604 "172.31.5.2:9200".to_string(),
605 "172.31.5.11:9200".to_string(),
606 ]
607 );
608
609 clear_site_properties(&path);
610 }
611
612 #[test]
616 fn site_properties_resolve_without_explicit_addr() {
617 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
618 let path = site_properties("site_only", SITE_HA);
619
620 let (master_addr, master_addrs) = resolve(None).expect("resolution must succeed");
621
622 assert_eq!(master_addr, "172.31.5.10:9200");
623 assert_eq!(master_addrs.len(), 3);
624
625 clear_site_properties(&path);
626 }
627
628 #[test]
631 fn env_master_addr_outranks_site_properties_and_uri_authority() {
632 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
633 let path = site_properties("env_over_site", SITE_HA);
634 unsafe { std::env::set_var(ENV_MASTER_ADDR, "10.0.0.7:9200") };
635
636 let (master_addr, master_addrs) =
637 resolve(Some("192.0.2.5:9999")).expect("resolution must succeed");
638
639 assert_eq!(master_addr, "10.0.0.7:9200");
640 assert!(
641 master_addrs.is_empty(),
642 "a single env address must not populate the HA list, got {master_addrs:?}"
643 );
644
645 clear_site_properties(&path);
646 }
647
648 #[test]
651 fn explicit_addr_applies_when_no_source_declares_masters() {
652 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
653 let path = site_properties("explicit_only", SITE_NO_MASTER);
654
655 let (master_addr, master_addrs) =
656 resolve(Some("10.0.0.1:9200,10.0.0.2:9200")).expect("resolution must succeed");
657
658 assert_eq!(master_addr, "10.0.0.1:9200");
659 assert_eq!(
660 master_addrs,
661 vec!["10.0.0.1:9200".to_string(), "10.0.0.2:9200".to_string()]
662 );
663
664 clear_site_properties(&path);
665 }
666
667 #[test]
670 fn missing_master_addr_fails() {
671 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
672 let path = site_properties("missing_master", SITE_NO_MASTER);
673
674 let err = resolve(None).expect_err("resolution must fail without any master address");
675 assert_eq!(err.kind(), ErrorKind::ConfigInvalid);
676 assert!(
677 err.to_string().contains("master_addr is not configured"),
678 "unexpected error message: {err}"
679 );
680
681 clear_site_properties(&path);
682 }
683
684 #[test]
688 fn env_master_addr_accepts_gfs_uri_form() {
689 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
690 let path = site_properties("env_gfs_uri", SITE_NO_MASTER);
691 unsafe { std::env::set_var(ENV_MASTER_ADDR, "gfs://10.0.0.1:9200,10.0.0.2:9200/lance") };
692
693 let (master_addr, master_addrs) =
694 resolve(Some("192.0.2.5:9999")).expect("resolution must succeed");
695
696 assert_eq!(master_addr, "10.0.0.1:9200");
697 assert_eq!(master_addrs.len(), 2);
698
699 clear_site_properties(&path);
700 }
701
702 #[test]
703 fn site_properties_declare_master_reads_both_keys() {
704 assert!(site_properties_declare_master(
705 "goosefs.master.rpc.addresses=10.0.0.1:9200,10.0.0.2:9200\n"
706 ));
707 assert!(site_properties_declare_master(
708 "goosefs.master.hostname = master-1\ngoosefs.master.rpc.port=9200\n"
709 ));
710 assert!(site_properties_declare_master(
712 "goosefs.master.hostname:master-1\n"
713 ));
714 }
715
716 #[test]
717 fn site_properties_declare_master_rejects_absent_and_blank_keys() {
718 assert!(!site_properties_declare_master(SITE_NO_MASTER));
719 assert!(!site_properties_declare_master(
720 "# goosefs.master.rpc.addresses=10.0.0.1:9200\n! goosefs.master.hostname=master-1\n"
721 ));
722 assert!(!site_properties_declare_master(
723 "goosefs.master.hostname=\n"
724 ));
725 assert!(!site_properties_declare_master(
726 "goosefs.master.rpc.addresses= , ,\n"
727 ));
728 assert!(!site_properties_declare_master(
732 "goosefs.master.rpc.addresses=\ngoosefs.master.hostname=master-1\n"
733 ));
734 }
735
736 #[test]
737 fn test_builder_build() {
738 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
739 let builder = GoosefsBuilder::default()
740 .root("/data")
741 .master_addr("127.0.0.1:9200")
742 .build();
743 assert!(builder.is_ok());
744 }
745
746 #[test]
747 fn test_builder_ha() {
748 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
749 let builder = GoosefsBuilder::default()
750 .root("/data")
751 .master_addr("10.0.0.1:9200,10.0.0.2:9200,10.0.0.3:9200")
752 .build();
753 assert!(builder.is_ok());
754 }
755
756 #[test]
762 fn test_builder_blank_master_addr_fails() {
763 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
764 let err = GoosefsBuilder::default()
765 .root("/data")
766 .master_addr(" , , ")
767 .build()
768 .expect_err("build must fail when master_addr is blank");
769 assert_eq!(err.kind(), ErrorKind::ConfigInvalid);
770 assert!(
771 err.to_string().contains("master_addr is empty"),
772 "unexpected error message: {err}"
773 );
774 }
775
776 #[test]
777 fn test_builder_unknown_write_type_fails() {
778 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
779 for wt in ["INVALID_WT", "cache_throughh"] {
780 let err = GoosefsBuilder::default()
781 .root("/data")
782 .master_addr("127.0.0.1:9200")
783 .write_type(wt)
784 .build()
785 .expect_err("build must fail on unknown write_type");
786 assert_eq!(err.kind(), ErrorKind::ConfigInvalid);
787 assert!(
788 err.to_string().contains("write_type"),
789 "unexpected error message for {wt}: {err}"
790 );
791 }
792 }
793
794 #[test]
795 fn test_builder_write_type_is_case_insensitive() {
796 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
797 let backend = GoosefsBuilder::default()
798 .root("/data")
799 .master_addr("127.0.0.1:9200")
800 .write_type("CACHE_THROUGH")
801 .build();
802 assert!(backend.is_ok());
803 }
804
805 #[test]
806 fn test_capability_rename_with_if_not_exists() {
807 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
808 let backend = GoosefsBuilder::default()
809 .root("/data")
810 .master_addr("127.0.0.1:9200")
811 .build()
812 .expect("build");
813 let cap = backend.capability();
814 assert!(cap.write_with_if_not_exists);
815 assert!(
816 cap.rename_with_if_not_exists,
817 "rename_with_if_not_exists must be declared for Create publish"
818 );
819 }
820}