Skip to main content

lance_namespace_impls/dir/
builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use lance::session::Session;
8use lance_core::Result;
9use lance_core::utils::parse::str_to_bool;
10use lance_io::object_store::{ObjectStore, ObjectStoreParams, ObjectStoreRegistry};
11use lance_namespace::error::NamespaceError;
12use object_store::path::Path;
13use tokio::sync::OnceCell;
14
15use super::{DirectoryNamespace, OpsMetrics, manifest, manifest_feature_flags};
16use crate::context::DynamicContextProvider;
17use crate::credentials::{create_credential_vendor_for_location, has_credential_vendor_config};
18
19/// Builder for creating a DirectoryNamespace.
20///
21/// This builder provides a fluent API for configuring and establishing
22/// connections to directory-based Lance namespaces.
23///
24/// # Examples
25///
26/// ```no_run
27/// # use lance_namespace_impls::DirectoryNamespaceBuilder;
28/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
29/// // Create a local directory namespace
30/// let namespace = DirectoryNamespaceBuilder::new("/path/to/data")
31///     .build()
32///     .await?;
33/// # Ok(())
34/// # }
35/// ```
36///
37/// ```no_run
38/// # use lance_namespace_impls::DirectoryNamespaceBuilder;
39/// # use lance::session::Session;
40/// # use std::sync::Arc;
41/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
42/// // Create with custom storage options and session
43/// let session = Arc::new(Session::default());
44/// let namespace = DirectoryNamespaceBuilder::new("s3://bucket/path")
45///     .storage_option("region", "us-west-2")
46///     .storage_option("access_key_id", "key")
47///     .session(session)
48///     .build()
49///     .await?;
50/// # Ok(())
51/// # }
52/// ```
53#[derive(Clone)]
54pub struct DirectoryNamespaceBuilder {
55    root: String,
56    pub(super) storage_options: Option<HashMap<String, String>>,
57    session: Option<Arc<Session>>,
58    pub(super) manifest_enabled: bool,
59    pub(super) dir_listing_enabled: bool,
60    pub(super) inline_optimization_enabled: bool,
61    table_version_tracking_enabled: bool,
62    /// When true, enables migration mode where the namespace checks the manifest first
63    /// before falling back to directory listing for root-level tables. When false (default),
64    /// root-level tables use directory listing directly without checking the manifest,
65    /// avoiding extra object store calls.
66    dir_listing_to_manifest_migration_enabled: bool,
67    credential_vendor_properties: HashMap<String, String>,
68    context_provider: Option<Arc<dyn DynamicContextProvider>>,
69    commit_retries: Option<u32>,
70    /// When true, returns input storage options in describe_table/declare_table responses
71    /// when no credential vendor is configured. Useful for testing. Default: false.
72    vend_input_storage_options: bool,
73    /// When set, adds expires_at_millis to vended storage options. The value is calculated
74    /// as current_time_millis + this interval. This allows clients to know when to refresh
75    /// credentials by calling describe_table again. Only effective when vend_input_storage_options
76    /// is true.
77    vend_input_storage_options_refresh_interval_millis: Option<u64>,
78    /// When true, tracks operation metrics. Default: false.
79    ops_metrics_enabled: bool,
80}
81
82impl std::fmt::Debug for DirectoryNamespaceBuilder {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.debug_struct("DirectoryNamespaceBuilder")
85            .field("root", &self.root)
86            .field("storage_options", &self.storage_options)
87            .field("manifest_enabled", &self.manifest_enabled)
88            .field("dir_listing_enabled", &self.dir_listing_enabled)
89            .field(
90                "inline_optimization_enabled",
91                &self.inline_optimization_enabled,
92            )
93            .field(
94                "table_version_tracking_enabled",
95                &self.table_version_tracking_enabled,
96            )
97            .field(
98                "dir_listing_to_manifest_migration_enabled",
99                &self.dir_listing_to_manifest_migration_enabled,
100            )
101            .field(
102                "context_provider",
103                &self.context_provider.as_ref().map(|_| "Some(...)"),
104            )
105            .field(
106                "vend_input_storage_options",
107                &self.vend_input_storage_options,
108            )
109            .field(
110                "vend_input_storage_options_refresh_interval_millis",
111                &self.vend_input_storage_options_refresh_interval_millis,
112            )
113            .field("ops_metrics_enabled", &self.ops_metrics_enabled)
114            .finish()
115    }
116}
117
118impl DirectoryNamespaceBuilder {
119    /// Create a new DirectoryNamespaceBuilder with the specified root path.
120    ///
121    /// # Arguments
122    ///
123    /// * `root` - Root directory path (local path or cloud URI like s3://bucket/path)
124    pub fn new(root: impl Into<String>) -> Self {
125        Self {
126            root: root.into().trim_end_matches('/').to_string(),
127            storage_options: None,
128            session: None,
129            manifest_enabled: true,
130            dir_listing_enabled: true, // Default to enabled for backwards compatibility
131            inline_optimization_enabled: false,
132            table_version_tracking_enabled: false, // Default to disabled
133            dir_listing_to_manifest_migration_enabled: false, // Default to disabled
134            credential_vendor_properties: HashMap::new(),
135            context_provider: None,
136            commit_retries: None,
137            vend_input_storage_options: false,
138            vend_input_storage_options_refresh_interval_millis: None,
139            ops_metrics_enabled: false,
140        }
141    }
142
143    /// Enable or disable manifest-based listing.
144    ///
145    /// When enabled (default), the namespace uses a `__manifest` table to track tables.
146    /// When disabled, relies solely on directory scanning.
147    pub fn manifest_enabled(mut self, enabled: bool) -> Self {
148        self.manifest_enabled = enabled;
149        self
150    }
151
152    /// Enable or disable directory-based listing fallback.
153    ///
154    /// When enabled (default), falls back to directory scanning for tables not in the manifest.
155    /// When disabled, only consults the manifest table.
156    pub fn dir_listing_enabled(mut self, enabled: bool) -> Self {
157        self.dir_listing_enabled = enabled;
158        self
159    }
160
161    /// Enable or disable migration mode from directory listing to manifest.
162    ///
163    /// When enabled, root-level table operations check the manifest first before
164    /// falling back to directory listing. When disabled (default), root-level tables
165    /// use directory listing directly, avoiding extra object store calls.
166    /// Only relevant when both `manifest_enabled` and `dir_listing_enabled` are true.
167    pub fn dir_listing_to_manifest_migration_enabled(mut self, enabled: bool) -> Self {
168        self.dir_listing_to_manifest_migration_enabled = enabled;
169        self
170    }
171
172    /// Enable or disable replacement index maintenance for the __manifest table.
173    ///
174    /// When enabled, copy-on-write manifest rewrites build replacement indices for fast
175    /// reads. This is disabled by default so rewrites only replace data files.
176    pub fn inline_optimization_enabled(mut self, enabled: bool) -> Self {
177        self.inline_optimization_enabled = enabled;
178        self
179    }
180
181    /// Enable or disable table version tracking through the namespace.
182    ///
183    /// When enabled, `describe_table` returns `managed_versioning: true` to indicate
184    /// that commits should go through the namespace's table version APIs rather than
185    /// direct object store operations.
186    ///
187    /// When disabled (default), `managed_versioning` is not set.
188    pub fn table_version_tracking_enabled(mut self, enabled: bool) -> Self {
189        self.table_version_tracking_enabled = enabled;
190        self
191    }
192
193    /// Create a DirectoryNamespaceBuilder from properties HashMap.
194    ///
195    /// This method parses a properties map into builder configuration.
196    /// It expects:
197    /// - `root`: The root directory path (required)
198    /// - `manifest_enabled`: Enable manifest-based table tracking (optional, default: true)
199    /// - `dir_listing_enabled`: Enable directory listing for table discovery (optional, default: true)
200    /// - `inline_optimization_enabled`: Enable replacement indices on __manifest rewrites (optional, default: false)
201    /// - `storage.*`: Storage options (optional, prefix will be stripped)
202    ///
203    /// Credential vendor properties (prefixed with `credential_vendor.`, prefix is stripped):
204    /// - `credential_vendor.enabled`: Set to "true" to enable credential vending (required)
205    /// - `credential_vendor.permission`: Permission level: read, write, or admin (default: read)
206    ///
207    /// AWS-specific properties (for s3:// locations):
208    /// - `credential_vendor.aws_role_arn`: AWS IAM role ARN (required for AWS)
209    /// - `credential_vendor.aws_external_id`: AWS external ID (optional)
210    /// - `credential_vendor.aws_region`: AWS region (optional)
211    /// - `credential_vendor.aws_role_session_name`: AWS role session name (optional)
212    /// - `credential_vendor.aws_duration_millis`: Credential duration in ms (default: 3600000, range: 15min-12hrs)
213    ///
214    /// GCP-specific properties (for gs:// locations):
215    /// - `credential_vendor.gcp_service_account`: Service account to impersonate (optional)
216    /// - `credential_vendor.gcp_workload_identity_provider`: Workload Identity Provider for OIDC token exchange (optional)
217    /// - `credential_vendor.gcp_impersonation_service_account`: Service account to impersonate after workload identity exchange (optional)
218    ///
219    /// Note: GCP uses Application Default Credentials (ADC). To use a service account key file,
220    /// set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable before starting.
221    /// GCP token duration cannot be configured; it's determined by the STS endpoint (typically 1 hour).
222    ///
223    /// Azure-specific properties (for az:// locations):
224    /// - `credential_vendor.azure_account_name`: Azure storage account name (required for Azure)
225    /// - `credential_vendor.azure_tenant_id`: Azure tenant ID (optional)
226    /// - `credential_vendor.azure_federated_client_id`: Client ID used for workload identity federation (optional)
227    /// - `credential_vendor.azure_duration_millis`: Credential duration in ms (default: 3600000, up to 7 days)
228    ///
229    /// # Arguments
230    ///
231    /// * `properties` - Configuration properties
232    /// * `session` - Optional Lance session to reuse object store registry
233    ///
234    /// # Returns
235    ///
236    /// Returns a `DirectoryNamespaceBuilder` instance.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if the `root` property is missing.
241    ///
242    /// # Examples
243    ///
244    /// ```no_run
245    /// # use lance_namespace_impls::DirectoryNamespaceBuilder;
246    /// # use std::collections::HashMap;
247    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
248    /// let mut properties = HashMap::new();
249    /// properties.insert("root".to_string(), "/path/to/data".to_string());
250    /// properties.insert("manifest_enabled".to_string(), "true".to_string());
251    /// properties.insert("dir_listing_enabled".to_string(), "false".to_string());
252    /// properties.insert("storage.region".to_string(), "us-west-2".to_string());
253    ///
254    /// let namespace = DirectoryNamespaceBuilder::from_properties(properties, None)?
255    ///     .build()
256    ///     .await?;
257    /// # Ok(())
258    /// # }
259    /// ```
260    pub fn from_properties(
261        properties: HashMap<String, String>,
262        session: Option<Arc<Session>>,
263    ) -> Result<Self> {
264        // Extract root from properties (required)
265        let root = properties.get("root").cloned().ok_or_else(|| {
266            lance_core::Error::from(NamespaceError::InvalidInput {
267                message: "Missing required property 'root' for directory namespace".to_string(),
268            })
269        })?;
270
271        // Extract storage options (properties prefixed with "storage.")
272        let storage_options: HashMap<String, String> = properties
273            .iter()
274            .filter_map(|(k, v)| {
275                k.strip_prefix("storage.")
276                    .map(|key| (key.to_string(), v.clone()))
277            })
278            .collect();
279
280        let storage_options = if storage_options.is_empty() {
281            None
282        } else {
283            Some(storage_options)
284        };
285
286        // Extract manifest_enabled (default: true)
287        let manifest_enabled = properties
288            .get("manifest_enabled")
289            .and_then(|v| str_to_bool(v))
290            .unwrap_or(true);
291
292        // Extract dir_listing_enabled (default: true)
293        let dir_listing_enabled = properties
294            .get("dir_listing_enabled")
295            .and_then(|v| str_to_bool(v))
296            .unwrap_or(true);
297
298        // Extract inline_optimization_enabled (default: false)
299        let inline_optimization_enabled = properties
300            .get("inline_optimization_enabled")
301            .and_then(|v| str_to_bool(v))
302            .unwrap_or(false);
303
304        // Extract table_version_tracking_enabled (default: false)
305        let table_version_tracking_enabled = properties
306            .get("table_version_tracking_enabled")
307            .and_then(|v| str_to_bool(v))
308            .unwrap_or(false);
309
310        // Extract dir_listing_to_manifest_migration_enabled (default: false)
311        let dir_listing_to_manifest_migration_enabled = properties
312            .get("dir_listing_to_manifest_migration_enabled")
313            .and_then(|v| str_to_bool(v))
314            .unwrap_or(false);
315
316        // Extract credential vendor properties (properties prefixed with "credential_vendor.")
317        // The prefix is stripped to get short property names
318        // The build() method will check if enabled=true before creating the vendor
319        let credential_vendor_properties: HashMap<String, String> = properties
320            .iter()
321            .filter_map(|(k, v)| {
322                k.strip_prefix("credential_vendor.")
323                    .map(|key| (key.to_string(), v.clone()))
324            })
325            .collect();
326
327        let commit_retries = properties
328            .get("commit_retries")
329            .and_then(|v| v.parse::<u32>().ok());
330
331        // Extract vend_input_storage_options (default: false)
332        let vend_input_storage_options = properties
333            .get("vend_input_storage_options")
334            .and_then(|v| str_to_bool(v))
335            .unwrap_or(false);
336
337        // Extract vend_input_storage_options_refresh_interval_millis (optional)
338        let vend_input_storage_options_refresh_interval_millis = properties
339            .get("vend_input_storage_options_refresh_interval_millis")
340            .and_then(|v| v.parse::<u64>().ok());
341
342        // Extract ops_metrics_enabled (default: false)
343        let ops_metrics_enabled = properties
344            .get("ops_metrics_enabled")
345            .and_then(|v| str_to_bool(v))
346            .unwrap_or(false);
347
348        Ok(Self {
349            root: root.trim_end_matches('/').to_string(),
350            storage_options,
351            session,
352            manifest_enabled,
353            dir_listing_enabled,
354            inline_optimization_enabled,
355            table_version_tracking_enabled,
356            dir_listing_to_manifest_migration_enabled,
357            credential_vendor_properties,
358            context_provider: None,
359            commit_retries,
360            vend_input_storage_options,
361            vend_input_storage_options_refresh_interval_millis,
362            ops_metrics_enabled,
363        })
364    }
365
366    /// Add a storage option.
367    ///
368    /// # Arguments
369    ///
370    /// * `key` - Storage option key (e.g., "region", "access_key_id")
371    /// * `value` - Storage option value
372    pub fn storage_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
373        self.storage_options
374            .get_or_insert_with(HashMap::new)
375            .insert(key.into(), value.into());
376        self
377    }
378
379    /// Add multiple storage options.
380    ///
381    /// # Arguments
382    ///
383    /// * `options` - HashMap of storage options to add
384    pub fn storage_options(mut self, options: HashMap<String, String>) -> Self {
385        self.storage_options
386            .get_or_insert_with(HashMap::new)
387            .extend(options);
388        self
389    }
390
391    /// Set the Lance session to use for this namespace.
392    ///
393    /// When a session is provided, the namespace will reuse the session's
394    /// object store registry, allowing multiple namespaces and datasets
395    /// to share the same underlying storage connections.
396    ///
397    /// # Arguments
398    ///
399    /// * `session` - Arc-wrapped Lance session
400    pub fn session(mut self, session: Arc<Session>) -> Self {
401        self.session = Some(session);
402        self
403    }
404
405    /// Set the number of retries for commit operations on the manifest table.
406    /// If not set, defaults to [`lance_table::io::commit::CommitConfig`] default (20).
407    pub fn commit_retries(mut self, retries: u32) -> Self {
408        self.commit_retries = Some(retries);
409        self
410    }
411
412    /// Add a credential vendor property.
413    ///
414    /// Use short property names without the `credential_vendor.` prefix.
415    /// Common properties: `enabled`, `permission`.
416    /// AWS properties: `aws_role_arn`, `aws_external_id`, `aws_region`, `aws_role_session_name`, `aws_duration_millis`.
417    /// GCP properties: `gcp_service_account`, `gcp_workload_identity_provider`, `gcp_impersonation_service_account`.
418    /// Azure properties: `azure_account_name`, `azure_tenant_id`, `azure_federated_client_id`, `azure_duration_millis`.
419    ///
420    /// # Arguments
421    ///
422    /// * `key` - Property key (e.g., "enabled", "aws_role_arn")
423    /// * `value` - Property value
424    ///
425    /// # Example
426    ///
427    /// ```no_run
428    /// # use lance_namespace_impls::DirectoryNamespaceBuilder;
429    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
430    /// let namespace = DirectoryNamespaceBuilder::new("s3://my-bucket/data")
431    ///     .credential_vendor_property("enabled", "true")
432    ///     .credential_vendor_property("aws_role_arn", "arn:aws:iam::123456789012:role/MyRole")
433    ///     .credential_vendor_property("permission", "read")
434    ///     .build()
435    ///     .await?;
436    /// # Ok(())
437    /// # }
438    /// ```
439    pub fn credential_vendor_property(
440        mut self,
441        key: impl Into<String>,
442        value: impl Into<String>,
443    ) -> Self {
444        self.credential_vendor_properties
445            .insert(key.into(), value.into());
446        self
447    }
448
449    /// Add multiple credential vendor properties.
450    ///
451    /// Use short property names without the `credential_vendor.` prefix.
452    ///
453    /// # Arguments
454    ///
455    /// * `properties` - HashMap of credential vendor properties to add
456    pub fn credential_vendor_properties(mut self, properties: HashMap<String, String>) -> Self {
457        self.credential_vendor_properties.extend(properties);
458        self
459    }
460
461    /// Set a dynamic context provider for per-request context.
462    ///
463    /// The provider can be used to generate additional context for operations.
464    /// For DirectoryNamespace, the context is stored but not directly used
465    /// in operations (unlike RestNamespace where it's converted to HTTP headers).
466    ///
467    /// # Arguments
468    ///
469    /// * `provider` - The context provider implementation
470    pub fn context_provider(mut self, provider: Arc<dyn DynamicContextProvider>) -> Self {
471        self.context_provider = Some(provider);
472        self
473    }
474
475    /// Enable or disable returning input storage options in responses.
476    ///
477    /// When enabled, `describe_table` and `declare_table` will return the storage
478    /// options passed to the builder when no credential vendor is configured.
479    /// This is useful for testing scenarios where you want to pass storage options
480    /// through to clients.
481    ///
482    /// Default is false (storage options are not returned unless credential vending is configured).
483    pub fn vend_input_storage_options(mut self, enabled: bool) -> Self {
484        self.vend_input_storage_options = enabled;
485        self
486    }
487
488    /// Set the refresh interval for vended input storage options.
489    ///
490    /// When set, vended storage options will include an `expires_at_millis` field
491    /// calculated as `current_time_millis + interval_millis`. This allows clients
492    /// to know when to refresh credentials by calling `describe_table` again.
493    ///
494    /// This only has effect when `vend_input_storage_options` is enabled.
495    ///
496    /// # Arguments
497    ///
498    /// * `interval_millis` - The refresh interval in milliseconds
499    pub fn vend_input_storage_options_refresh_interval_millis(
500        mut self,
501        interval_millis: u64,
502    ) -> Self {
503        self.vend_input_storage_options_refresh_interval_millis = Some(interval_millis);
504        self
505    }
506
507    /// Enable or disable operation metrics tracking.
508    ///
509    /// When enabled, the namespace will track how many times each API operation
510    /// is called. Use `retrieve_ops_metrics()` on the built namespace to get
511    /// the current counts.
512    ///
513    /// Default is false.
514    pub fn ops_metrics_enabled(mut self, enabled: bool) -> Self {
515        self.ops_metrics_enabled = enabled;
516        self
517    }
518
519    /// Build the DirectoryNamespace.
520    ///
521    /// # Returns
522    ///
523    /// Returns a `DirectoryNamespace` instance.
524    ///
525    /// # Errors
526    ///
527    /// Returns an error if:
528    /// - The root path is invalid
529    /// - Connection to the storage backend fails
530    /// - Storage options are invalid
531    pub async fn build(self) -> Result<DirectoryNamespace> {
532        let (object_store, base_path) =
533            Self::initialize_object_store(&self.root, &self.storage_options, &self.session).await?;
534
535        let manifest_ns = if self.manifest_enabled {
536            match manifest::ManifestNamespace::open_from_directory(
537                self.root.clone(),
538                self.storage_options.clone(),
539                self.session.clone(),
540                object_store.clone(),
541                base_path.clone(),
542                self.dir_listing_enabled,
543                self.inline_optimization_enabled,
544                self.commit_retries,
545            )
546            .await
547            {
548                Ok(ns) => Some(Arc::new(ns)),
549                Err(e) if manifest_feature_flags::is_incompatible_manifest_error(&e) => {
550                    // The manifest exists but was written with a feature flag this
551                    // build does not understand. Refuse rather than silently
552                    // degrading to a directory-listing view that ignores it.
553                    return Err(e);
554                }
555                Err(e) if manifest::ManifestNamespace::is_not_found_load_error(&e) => {
556                    log::debug!("Manifest namespace does not exist yet: {}", e);
557                    None
558                }
559                Err(e) => return Err(e),
560            }
561        } else {
562            None
563        };
564        let manifest_cell = OnceCell::new();
565        if let Some(manifest_ns) = manifest_ns {
566            let _ = manifest_cell.set(manifest_ns);
567        }
568
569        // Create credential vendor once during initialization if enabled
570        let credential_vendor = if has_credential_vendor_config(&self.credential_vendor_properties)
571        {
572            create_credential_vendor_for_location(&self.root, &self.credential_vendor_properties)
573                .await?
574                .map(Arc::from)
575        } else {
576            None
577        };
578
579        let ops_metrics = if self.ops_metrics_enabled {
580            Some(Arc::new(OpsMetrics::default()))
581        } else {
582            None
583        };
584
585        Ok(DirectoryNamespace {
586            root: self.root,
587            storage_options: self.storage_options,
588            session: self.session,
589            object_store,
590            base_path,
591            manifest_ns: manifest_cell,
592            write_manifest_ns: OnceCell::new(),
593            manifest_enabled: self.manifest_enabled,
594            dir_listing_enabled: self.dir_listing_enabled,
595            inline_optimization_enabled: self.inline_optimization_enabled,
596            commit_retries: self.commit_retries,
597            dir_listing_to_manifest_migration_enabled: self
598                .dir_listing_to_manifest_migration_enabled,
599            table_version_tracking_enabled: self.table_version_tracking_enabled,
600            credential_vendor,
601            context_provider: self.context_provider,
602            vend_input_storage_options: self.vend_input_storage_options,
603            vend_input_storage_options_refresh_interval_millis: self
604                .vend_input_storage_options_refresh_interval_millis,
605            ops_metrics,
606        })
607    }
608
609    /// Initialize the Lance ObjectStore based on the configuration
610    async fn initialize_object_store(
611        root: &str,
612        storage_options: &Option<HashMap<String, String>>,
613        session: &Option<Arc<Session>>,
614    ) -> Result<(Arc<ObjectStore>, Path)> {
615        // Build ObjectStoreParams from storage options
616        let accessor = storage_options.clone().map(|opts| {
617            Arc::new(lance_io::object_store::StorageOptionsAccessor::with_static_options(opts))
618        });
619        let params = ObjectStoreParams {
620            storage_options_accessor: accessor,
621            ..Default::default()
622        };
623
624        // Use object store registry from session if provided, otherwise create a new one
625        let registry = if let Some(session) = session {
626            session.store_registry()
627        } else {
628            Arc::new(ObjectStoreRegistry::default())
629        };
630
631        // Use Lance's object store factory to create from URI
632        let (object_store, base_path) = ObjectStore::from_uri_and_params(registry, root, &params)
633            .await
634            .map_err(|e| {
635                lance_core::Error::from(NamespaceError::Internal {
636                    message: format!("Failed to create object store: {:?}", e),
637                })
638            })?;
639
640        Ok((object_store, base_path))
641    }
642}