Skip to main content

opendal_service_goosefs/
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;
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/// [GooseFS](https://cloud.tencent.com/product/goosefs) services support via native gRPC.
35#[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    /// Set root of this backend.
51    ///
52    /// All operations will happen under this root.
53    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    /// Set master address(es).
63    ///
64    /// Single master: `"10.0.0.1:9200"`
65    /// HA (comma-separated): `"10.0.0.1:9200,10.0.0.2:9200,10.0.0.3:9200"`
66    ///
67    /// When provided here it **overrides** any address discovered from
68    /// `goosefs-site.properties` or `GOOSEFS_MASTER_ADDR`. When left unset
69    /// the builder falls back to the SDK's auto-discovery chain
70    /// (defaults → properties → env) — see
71    /// [goosefs-sdk `docs/CLIENT_CONFIGURATION.md`](https://github.com/Tencent/tencent-goosefs-rust-sdk/blob/main/docs/CLIENT_CONFIGURATION.md)
72    /// §1. `build()` fails with `ConfigInvalid` only if **no** source
73    /// supplies a master address.
74    pub fn master_addr(mut self, addr: &str) -> Self {
75        if !addr.is_empty() {
76            self.config.master_addr = Some(addr.to_string());
77        }
78        self
79    }
80
81    /// Set block size for new files (bytes).
82    pub fn block_size(mut self, size: u64) -> Self {
83        self.config.block_size = Some(size);
84        self
85    }
86
87    /// Set chunk size for streaming RPCs (bytes).
88    pub fn chunk_size(mut self, size: u64) -> Self {
89        self.config.chunk_size = Some(size);
90        self
91    }
92
93    /// Set default write type.
94    ///
95    /// Values: `"must_cache"`, `"cache_through"`, `"through"`, `"async_through"`
96    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    /// Set authentication type.
104    ///
105    /// Values: `"nosasl"`, `"simple"` (default: `"simple"`).
106    /// - `"nosasl"` — skip SASL authentication entirely.
107    /// - `"simple"` — PLAIN SASL with username (server does not verify password).
108    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    /// Set authentication username.
116    ///
117    /// Used in SIMPLE mode as the login identity.
118    /// Default: current OS user (`$USER` / `$USERNAME`).
119    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
127impl Builder for GoosefsBuilder {
128    type Config = GoosefsConfig;
129
130    /// Build the backend and return a GoosefsBackend.
131    fn build(self) -> Result<impl Service> {
132        debug!("GoosefsBuilder::build started: {:?}", self);
133
134        let root = normalize_root(&self.config.root.clone().unwrap_or_default());
135        debug!("GoosefsBuilder use root {}", root);
136
137        // ── Step 1: establish the base SDK config ─────────────────────────────
138        //
139        // We follow the same priority chain that `FileSystemContext::connect`
140        // (and its `ConfigRefresher`) uses — see
141        // https://github.com/Tencent/tencent-goosefs-rust-sdk/blob/main/docs/CLIENT_CONFIGURATION.md
142        // §1 "Configuration Loading Priority":
143        //
144        //   defaults  <  goosefs-site.properties  <  GOOSEFS_* env vars
145        //
146        // `GoosefsConfig::from_properties_auto()` already implements this
147        // chain and is the *same* function the SDK calls every 60s to refresh
148        // the transparent-acceleration switches. Using it here keeps the
149        // initial OpenDAL build and the in-process hot-reload semantically
150        // aligned — users who deploy `goosefs-site.properties` get the exact
151        // same config from both paths.
152        //
153        // Failure policy:
154        //   * no properties file found  → silently uses defaults + env
155        //     (`from_properties_auto` handles this internally)
156        //   * properties file present but malformed → hard-fail
157        //     (broken config must not be silently dropped)
158        //
159        // Builder-explicit fields (Step 2) have the final say, overriding
160        // anything discovered from properties / env.
161        let mut goosefs_config = goosefs_sdk::config::GoosefsConfig::from_properties_auto()
162            .map_err(|e| {
163                Error::new(
164                    ErrorKind::ConfigInvalid,
165                    format!("failed to auto-load goosefs config: {e}"),
166                )
167                .with_operation("Builder::build")
168                .with_context("service", GOOSEFS_SCHEME)
169            })?;
170
171        // Root always comes from OpenDAL (it's an OpenDAL-layer concept).
172        goosefs_config.root = root.clone();
173
174        // ── Step 2: overlay builder-explicit fields (authoritative) ───────────
175        if let Some(ref master_addr) = self.config.master_addr {
176            let addrs: Vec<String> = master_addr
177                .split(',')
178                .map(|s| s.trim().to_string())
179                .filter(|s| !s.is_empty())
180                .collect();
181
182            if addrs.is_empty() {
183                return Err(Error::new(
184                    ErrorKind::ConfigInvalid,
185                    "master_addr is empty after trimming",
186                )
187                .with_operation("Builder::build")
188                .with_context("service", GOOSEFS_SCHEME));
189            }
190
191            if addrs.len() == 1 {
192                goosefs_config.master_addr = addrs[0].clone();
193                goosefs_config.master_addrs = Vec::new();
194            } else {
195                goosefs_config.master_addr = addrs[0].clone();
196                goosefs_config.master_addrs = addrs;
197            }
198        }
199
200        // After properties/env auto-load + builder overlay, we must still
201        // have at least one usable master address. If we don't, fail fast
202        // with a message that points the user at all three sources.
203        if goosefs_config.master_addr.is_empty() && goosefs_config.master_addrs.is_empty() {
204            return Err(Error::new(
205                ErrorKind::ConfigInvalid,
206                "master_addr is not configured: set it via GoosefsBuilder::master_addr(...), \
207                 the `master_addr` config key, the GOOSEFS_MASTER_ADDR env var, \
208                 or `goosefs.master.hostname`/`goosefs.master.rpc.addresses` in goosefs-site.properties",
209            )
210            .with_operation("Builder::build")
211            .with_context("service", GOOSEFS_SCHEME));
212        }
213        debug!(
214            "GoosefsBuilder use master_addr {} (addrs={:?})",
215            goosefs_config.master_addr, goosefs_config.master_addrs
216        );
217
218        if let Some(block_size) = self.config.block_size {
219            goosefs_config.block_size = block_size;
220        }
221        if let Some(chunk_size) = self.config.chunk_size {
222            goosefs_config.chunk_size = chunk_size;
223        }
224
225        // Parse write_type string → goosefs_sdk::WritePType i32.
226        //
227        // Normalise case once up front so we don't need to enumerate both
228        // `must_cache` and `MUST_CACHE` branches — this mirrors how the
229        // GooseFS server-side config parser (`WritePType::valueOf`) treats
230        // the value as case-insensitive.
231        if let Some(ref wt) = self.config.write_type {
232            let wt_i32 = match wt.to_lowercase().as_str() {
233                "must_cache" => 1,
234                "try_cache" => 2,
235                "cache_through" => 3,
236                "through" => 4,
237                "async_through" => 5,
238                _ => 1, // default to MUST_CACHE
239            };
240            goosefs_config.write_type = Some(wt_i32);
241        }
242
243        // Parse auth_type string → goosefs_sdk::auth::AuthType
244        if let Some(ref auth_type_str) = self.config.auth_type {
245            goosefs_config = goosefs_config
246                .with_auth_type_str(auth_type_str)
247                .map_err(|e| {
248                    Error::new(
249                        ErrorKind::ConfigInvalid,
250                        format!("invalid auth_type: {}", e),
251                    )
252                    .with_operation("Builder::build")
253                    .with_context("service", GOOSEFS_SCHEME)
254                })?;
255        }
256
257        if let Some(ref auth_username) = self.config.auth_username {
258            goosefs_config = goosefs_config.with_auth_username(auth_username);
259        }
260
261        // ── Step 3: validate the final merged config ──────────────────────────
262        goosefs_config.validate().map_err(|e| {
263            Error::new(
264                ErrorKind::ConfigInvalid,
265                format!("invalid goosefs config: {e}"),
266            )
267            .with_operation("Builder::build")
268            .with_context("service", GOOSEFS_SCHEME)
269        })?;
270
271        Ok(GoosefsBackend {
272            core: Arc::new(GoosefsCore::new(
273                ServiceInfo::new(GOOSEFS_SCHEME, &root, ""),
274                Capability {
275                    stat: true,
276                    read: true,
277                    write: true,
278                    write_can_multi: true,
279                    // Authoritative Create: write-via-temp then
280                    // GoosefsCore::rename(..., if_not_exists=true), backed by Master
281                    // no-replace rename. Not CreateFile on the final path
282                    // (writes go to .opendal.tmp.*).
283                    write_with_if_not_exists: true,
284                    create_dir: true,
285                    delete: true,
286                    list: true,
287                    rename: true,
288                    rename_with_if_not_exists: true,
289                    shared: true,
290                    ..Default::default()
291                },
292                root,
293                goosefs_config,
294            )),
295        })
296    }
297}
298
299#[derive(Debug, Clone)]
300pub struct GoosefsBackend {
301    pub(crate) core: Arc<GoosefsCore>,
302}
303
304impl Service for GoosefsBackend {
305    type Reader = oio::StreamReader<GoosefsReader>;
306    type Writer = GoosefsWriters;
307    type Lister = oio::PageLister<GoosefsLister>;
308    type Deleter = oio::OneShotDeleter<GoosefsDeleter>;
309    type Copier = ();
310
311    fn info(&self) -> ServiceInfo {
312        self.core.info.clone()
313    }
314
315    fn capability(&self) -> Capability {
316        self.core.capability
317    }
318
319    async fn create_dir(
320        &self,
321        _ctx: &OperationContext,
322        path: &str,
323        _: OpCreateDir,
324    ) -> Result<RpCreateDir> {
325        self.core.create_dir(path).await?;
326        Ok(RpCreateDir::default())
327    }
328
329    async fn stat(&self, _ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
330        let file_info = self.core.get_status(path).await?;
331        Ok(RpStat::new(self.core.file_info_to_metadata(&file_info)))
332    }
333    fn read(&self, _ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
334        let output: oio::StreamReader<GoosefsReader> = {
335            Ok(oio::StreamReader::new(GoosefsReader::new(
336                self.clone(),
337                path,
338                args,
339            )))
340        }?;
341
342        Ok(output)
343    }
344
345    fn write(&self, _ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
346        let output: GoosefsWriters = {
347            let w = GoosefsWriter::new(self.core.clone(), args.clone(), path.to_string());
348            Ok(w)
349        }?;
350
351        Ok(output)
352    }
353
354    fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
355        let output: oio::OneShotDeleter<GoosefsDeleter> = {
356            Ok(oio::OneShotDeleter::new(GoosefsDeleter::new(
357                self.core.clone(),
358            )))
359        }?;
360
361        Ok(output)
362    }
363
364    fn list(&self, _ctx: &OperationContext, path: &str, _args: OpList) -> Result<Self::Lister> {
365        let output: oio::PageLister<GoosefsLister> = {
366            let l = GoosefsLister::new(self.core.clone(), path);
367            Ok(oio::PageLister::new(l))
368        }?;
369
370        Ok(output)
371    }
372
373    fn copy(
374        &self,
375        _ctx: &OperationContext,
376        _from: &str,
377        _to: &str,
378        _args: OpCopy,
379        _opts: OpCopier,
380    ) -> Result<Self::Copier> {
381        Err(Error::new(
382            ErrorKind::Unsupported,
383            "operation is not supported",
384        ))
385    }
386
387    async fn rename(
388        &self,
389        _ctx: &OperationContext,
390        from: &str,
391        to: &str,
392        args: OpRename,
393    ) -> Result<RpRename> {
394        self.core.rename(from, to, args.if_not_exists()).await?;
395        Ok(RpRename::default())
396    }
397
398    async fn presign(
399        &self,
400        _ctx: &OperationContext,
401        _path: &str,
402        _args: OpPresign,
403    ) -> Result<RpPresign> {
404        Err(Error::new(
405            ErrorKind::Unsupported,
406            "operation is not supported",
407        ))
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn test_builder_build() {
417        let builder = GoosefsBuilder::default()
418            .root("/data")
419            .master_addr("127.0.0.1:9200")
420            .build();
421        assert!(builder.is_ok());
422    }
423
424    #[test]
425    fn test_builder_ha() {
426        let builder = GoosefsBuilder::default()
427            .root("/data")
428            .master_addr("10.0.0.1:9200,10.0.0.2:9200,10.0.0.3:9200")
429            .build();
430        assert!(builder.is_ok());
431    }
432
433    /// `master_addr` is mandatory — `build()` must fail with `ConfigInvalid`
434    /// when it cannot be resolved from any source. This test exercises the
435    /// "explicitly blank" form (empty / whitespace / comma-only), which is
436    /// environment-independent: Step 2 short-circuits on a blank override
437    /// before any auto-load value can rescue it.
438    #[test]
439    fn test_builder_blank_master_addr_fails() {
440        let err = GoosefsBuilder::default()
441            .root("/data")
442            .master_addr("   ,  , ")
443            .build()
444            .expect_err("build must fail when master_addr is blank");
445        assert_eq!(err.kind(), ErrorKind::ConfigInvalid);
446        assert!(
447            err.to_string().contains("master_addr is empty"),
448            "unexpected error message: {err}"
449        );
450    }
451
452    #[test]
453    fn test_capability_rename_with_if_not_exists() {
454        let backend = GoosefsBuilder::default()
455            .root("/data")
456            .master_addr("127.0.0.1:9200")
457            .build()
458            .expect("build");
459        let cap = backend.capability();
460        assert!(cap.write_with_if_not_exists);
461        assert!(
462            cap.rename_with_if_not_exists,
463            "rename_with_if_not_exists must be declared for Create publish"
464        );
465    }
466}