1use std::borrow::Cow;
7use std::collections::{HashMap, HashSet};
8use std::ops::Range;
9#[cfg(unix)]
10use std::os::unix::fs::PermissionsExt;
11use std::pin::Pin;
12use std::str::FromStr;
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15
16use ::tracing::{Span, field::Empty, instrument};
17use async_trait::async_trait;
18use bytes::Bytes;
19use chrono::{DateTime, Utc};
20use futures::{FutureExt, Stream};
21use futures::{StreamExt, TryStreamExt, future, stream::BoxStream};
22use lance_core::deepsize::DeepSizeOf;
23use lance_core::error::LanceOptionExt;
24use lance_core::utils::parse::{parse_env_as_bool, str_is_truthy};
25use list_retry::ListRetryStream;
26use object_store::DynObjectStore;
27use object_store::ObjectStoreExt as OSObjectStoreExt;
28#[cfg(feature = "aws")]
29use object_store::aws::AwsCredentialProvider;
30use object_store::list::PaginatedListStore;
31#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
32use object_store::{ClientOptions, HeaderMap, HeaderValue};
33use object_store::{
34 ListResult, ObjectMeta, ObjectStore as OSObjectStore, PutMode, PutOptions, PutPayload,
35 path::Path,
36};
37use providers::local::FileStoreProvider;
38use providers::memory::MemoryStoreProvider;
39use tokio::io::AsyncWriteExt;
40use url::Url;
41
42use super::local::LocalObjectReader;
43#[cfg(target_os = "linux")]
44use crate::uring::{UringCurrentThreadReader, UringReader};
45#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
46pub(crate) mod dynamic_credentials;
47#[cfg(any(feature = "oss", feature = "huggingface", feature = "tos"))]
48pub(crate) mod dynamic_opendal;
49mod list_retry;
50#[cfg(feature = "metrics")]
51pub mod metrics;
52#[cfg(any(
53 feature = "aws",
54 feature = "gcp",
55 feature = "azure",
56 feature = "oss",
57 feature = "tencent",
58 feature = "huggingface",
59 feature = "tos",
60 feature = "goosefs",
61))]
62pub(crate) mod opendal_store;
63pub mod providers;
64pub(crate) mod read_dir;
65pub mod storage_options;
66#[cfg(test)]
67pub(crate) mod test_utils;
68pub mod throttle;
69mod tracing;
70use crate::object_reader::SmallReader;
71use crate::object_writer::{LocalWriter, WriteResult};
72use crate::traits::{WriteExt, Writer};
73use crate::utils::tracking_store::{IOTracker, IoStats};
74use crate::{object_reader::CloudObjectReader, object_writer::ObjectWriter, traits::Reader};
75use lance_core::{Error, Result};
76
77pub const DEFAULT_LOCAL_IO_PARALLELISM: usize = 8;
82pub const DEFAULT_CLOUD_IO_PARALLELISM: usize = 64;
84
85const SERVER_SIDE_COPY_ENABLED_ENV: &str = "LANCE_IO_SERVER_SIDE_COPY_ENABLED";
86
87const DEFAULT_LOCAL_BLOCK_SIZE: usize = 4 * 1024; #[cfg(any(
89 feature = "aws",
90 feature = "gcp",
91 feature = "azure",
92 feature = "oss",
93 feature = "tencent",
94 feature = "huggingface",
95 feature = "tos",
96 feature = "goosefs",
97))]
98const DEFAULT_CLOUD_BLOCK_SIZE: usize = 64 * 1024; pub static DEFAULT_MAX_IOP_SIZE: std::sync::LazyLock<u64> = std::sync::LazyLock::new(|| {
101 std::env::var("LANCE_MAX_IOP_SIZE")
102 .map(|val| val.parse().unwrap())
103 .unwrap_or(16 * 1024 * 1024)
104});
105
106pub const DEFAULT_DOWNLOAD_RETRY_COUNT: usize = 3;
107
108#[derive(Debug)]
109struct StreamCopyError {
110 stage: &'static str,
111 source_path: String,
112 destination_path: String,
113 source: Box<dyn std::error::Error + Send + Sync>,
114}
115
116impl std::fmt::Display for StreamCopyError {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 write!(
119 f,
120 "multipart_stream_copy failed during {} from {} to {}: {}",
121 self.stage, self.source_path, self.destination_path, self.source
122 )
123 }
124}
125
126impl std::error::Error for StreamCopyError {
127 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
128 Some(self.source.as_ref())
129 }
130}
131
132fn stream_copy_error(
133 stage: &'static str,
134 source_path: &Path,
135 destination_path: &Path,
136 source: impl std::error::Error + Send + Sync + 'static,
137) -> Error {
138 Error::io_source(Box::new(StreamCopyError {
139 stage,
140 source_path: source_path.to_string(),
141 destination_path: destination_path.to_string(),
142 source: Box::new(source),
143 }))
144}
145
146pub use providers::{ObjectStoreProvider, ObjectStoreRegistry};
147pub use read_dir::ReadDirOptions;
148pub use storage_options::{
149 BASE_SCOPED_OPTION_PREFIX, BaseScopedStorageOptionsProvider, EXPIRES_AT_MILLIS_KEY,
150 LanceNamespaceStorageOptionsProvider, REFRESH_OFFSET_MILLIS_KEY, StorageOptionsAccessor,
151 StorageOptionsProvider, has_base_scoped_options, parse_base_scoped_key,
152 resolve_base_scoped_options,
153};
154
155#[async_trait]
156pub trait ObjectStoreExt {
157 async fn exists(&self, path: &Path) -> Result<bool>;
159
160 fn read_dir_all<'a, 'b>(
164 &'a self,
165 dir_path: impl Into<&'b Path> + Send,
166 unmodified_since: Option<DateTime<Utc>>,
167 ) -> BoxStream<'a, Result<ObjectMeta>>;
168}
169
170#[async_trait]
171pub(super) trait LocalDirOperations: std::fmt::Debug + Send + Sync {
172 async fn remove_dir_all(&self, path: &Path) -> Result<()>;
173}
174
175#[async_trait]
176impl<O: OSObjectStore + ?Sized> ObjectStoreExt for O {
177 fn read_dir_all<'a, 'b>(
178 &'a self,
179 dir_path: impl Into<&'b Path> + Send,
180 unmodified_since: Option<DateTime<Utc>>,
181 ) -> BoxStream<'a, Result<ObjectMeta>> {
182 let output = self.list(Some(dir_path.into())).map_err(|e| e.into());
183 if let Some(unmodified_since_val) = unmodified_since {
184 output
185 .try_filter(move |file| future::ready(file.last_modified <= unmodified_since_val))
186 .boxed()
187 } else {
188 output.boxed()
189 }
190 }
191
192 async fn exists(&self, path: &Path) -> Result<bool> {
193 match self.head(path).await {
194 Ok(_) => Ok(true),
195 Err(object_store::Error::NotFound { path: _, source: _ }) => Ok(false),
196 Err(e) => Err(e.into()),
197 }
198 }
199}
200
201#[derive(Clone)]
203pub struct ObjectStore {
204 pub inner: Arc<dyn OSObjectStore>,
206 local_dir_operations: Option<Arc<dyn LocalDirOperations>>,
208 scheme: String,
209 block_size: usize,
210 max_iop_size: u64,
211 pub use_constant_size_upload_parts: bool,
214 pub list_is_lexically_ordered: bool,
217 io_parallelism: usize,
218 download_retry_count: usize,
220 io_tracker: IOTracker,
222 pub store_prefix: String,
226 pub(crate) paginated_lister: Option<Arc<dyn PaginatedListStore>>,
229}
230
231impl std::fmt::Debug for ObjectStore {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 f.debug_struct("ObjectStore")
235 .field("inner", &self.inner)
236 .field("scheme", &self.scheme)
237 .field("block_size", &self.block_size)
238 .field("max_iop_size", &self.max_iop_size)
239 .field(
240 "use_constant_size_upload_parts",
241 &self.use_constant_size_upload_parts,
242 )
243 .field("list_is_lexically_ordered", &self.list_is_lexically_ordered)
244 .field("io_parallelism", &self.io_parallelism)
245 .field("download_retry_count", &self.download_retry_count)
246 .field("io_tracker", &self.io_tracker)
247 .field("store_prefix", &self.store_prefix)
248 .field("paginated_lister", &self.paginated_lister.is_some())
249 .finish()
250 }
251}
252
253impl DeepSizeOf for ObjectStore {
254 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
255 self.scheme.deep_size_of_children(context) + self.block_size.deep_size_of_children(context)
260 }
261}
262
263impl std::fmt::Display for ObjectStore {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 write!(f, "ObjectStore({})", self.scheme)
266 }
267}
268
269pub trait WrappingObjectStore: std::fmt::Debug + Send + Sync {
270 fn wrap(&self, store_prefix: &str, original: Arc<dyn OSObjectStore>) -> Arc<dyn OSObjectStore>;
275
276 fn wrap_paginated(
299 &self,
300 store_prefix: &str,
301 original: Arc<dyn PaginatedListStore>,
302 ) -> Option<Arc<dyn PaginatedListStore>>;
303}
304
305#[derive(Debug, Clone)]
306pub struct ChainedWrappingObjectStore {
307 wrappers: Vec<Arc<dyn WrappingObjectStore>>,
308}
309
310impl ChainedWrappingObjectStore {
311 pub fn new(wrappers: Vec<Arc<dyn WrappingObjectStore>>) -> Self {
312 Self { wrappers }
313 }
314
315 pub fn add_wrapper(&mut self, wrapper: Arc<dyn WrappingObjectStore>) {
316 self.wrappers.push(wrapper);
317 }
318}
319
320impl WrappingObjectStore for ChainedWrappingObjectStore {
321 fn wrap(&self, store_prefix: &str, original: Arc<dyn OSObjectStore>) -> Arc<dyn OSObjectStore> {
322 self.wrappers
323 .iter()
324 .fold(original, |acc, wrapper| wrapper.wrap(store_prefix, acc))
325 }
326
327 fn wrap_paginated(
330 &self,
331 store_prefix: &str,
332 original: Arc<dyn PaginatedListStore>,
333 ) -> Option<Arc<dyn PaginatedListStore>> {
334 self.wrappers.iter().try_fold(original, |acc, wrapper| {
335 wrapper.wrap_paginated(store_prefix, acc)
336 })
337 }
338}
339
340#[derive(Debug, Clone)]
343pub struct ObjectStoreParams {
344 pub block_size: Option<usize>,
345 #[deprecated(note = "Implement an ObjectStoreProvider instead")]
346 pub object_store: Option<(Arc<DynObjectStore>, Url)>,
347 pub s3_credentials_refresh_offset: Duration,
350 #[cfg(feature = "aws")]
351 pub aws_credentials: Option<AwsCredentialProvider>,
352 pub object_store_wrapper: Option<Arc<dyn WrappingObjectStore>>,
353 pub storage_options_accessor: Option<Arc<StorageOptionsAccessor>>,
359 pub use_constant_size_upload_parts: bool,
364 pub list_is_lexically_ordered: Option<bool>,
365}
366
367impl Default for ObjectStoreParams {
368 fn default() -> Self {
369 #[allow(deprecated)]
370 Self {
371 object_store: None,
372 block_size: None,
373 s3_credentials_refresh_offset: Duration::from_secs(60),
374 #[cfg(feature = "aws")]
375 aws_credentials: None,
376 object_store_wrapper: None,
377 storage_options_accessor: None,
378 use_constant_size_upload_parts: false,
379 list_is_lexically_ordered: None,
380 }
381 }
382}
383
384impl ObjectStoreParams {
385 pub fn get_accessor(&self) -> Option<Arc<StorageOptionsAccessor>> {
387 self.storage_options_accessor.clone()
388 }
389
390 pub fn storage_options(&self) -> Option<&HashMap<String, String>> {
394 self.storage_options_accessor
395 .as_ref()
396 .and_then(|a| a.initial_storage_options())
397 }
398
399 pub fn resolved_block_size(&self) -> Result<Option<usize>> {
402 if self.block_size.is_some() {
403 return Ok(self.block_size);
404 }
405 match self.storage_options() {
406 Some(options) => StorageOptions(options.clone()).block_size(),
407 None => Ok(None),
408 }
409 }
410
411 pub fn scoped_to_base(&self, base_id: Option<u32>) -> Cow<'_, Self> {
418 let Some(accessor) = &self.storage_options_accessor else {
419 return Cow::Borrowed(self);
420 };
421 let scoped = accessor.scoped_to_base(base_id);
422 if Arc::ptr_eq(&scoped, accessor) {
423 Cow::Borrowed(self)
424 } else {
425 Cow::Owned(Self {
426 storage_options_accessor: Some(scoped),
427 ..self.clone()
428 })
429 }
430 }
431}
432
433fn wrapper_allocation_ptr(wrapper: &Arc<dyn WrappingObjectStore>) -> *const () {
434 Arc::as_ptr(wrapper) as *const ()
437}
438
439impl std::hash::Hash for ObjectStoreParams {
441 #[allow(deprecated)]
442 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
443 self.block_size.hash(state);
445 if let Some((store, url)) = &self.object_store {
446 Arc::as_ptr(store).hash(state);
447 url.hash(state);
448 }
449 self.s3_credentials_refresh_offset.hash(state);
450 #[cfg(feature = "aws")]
451 if let Some(aws_credentials) = &self.aws_credentials {
452 Arc::as_ptr(aws_credentials).hash(state);
453 }
454 if let Some(wrapper) = &self.object_store_wrapper {
455 wrapper_allocation_ptr(wrapper).hash(state);
456 }
457 if let Some(accessor) = &self.storage_options_accessor {
458 accessor.accessor_id().hash(state);
459 }
460 self.use_constant_size_upload_parts.hash(state);
461 self.list_is_lexically_ordered.hash(state);
462 }
463}
464
465impl Eq for ObjectStoreParams {}
467impl PartialEq for ObjectStoreParams {
468 #[allow(deprecated)]
469 fn eq(&self, other: &Self) -> bool {
470 #[cfg(feature = "aws")]
471 if self.aws_credentials.is_some() != other.aws_credentials.is_some() {
472 return false;
473 }
474
475 self.block_size == other.block_size
478 && self
479 .object_store
480 .as_ref()
481 .map(|(store, url)| (Arc::as_ptr(store), url))
482 == other
483 .object_store
484 .as_ref()
485 .map(|(store, url)| (Arc::as_ptr(store), url))
486 && self.s3_credentials_refresh_offset == other.s3_credentials_refresh_offset
487 && self
488 .object_store_wrapper
489 .as_ref()
490 .map(wrapper_allocation_ptr)
491 == other
492 .object_store_wrapper
493 .as_ref()
494 .map(wrapper_allocation_ptr)
495 && self
496 .storage_options_accessor
497 .as_ref()
498 .map(|a| a.accessor_id())
499 == other
500 .storage_options_accessor
501 .as_ref()
502 .map(|a| a.accessor_id())
503 && self.use_constant_size_upload_parts == other.use_constant_size_upload_parts
504 && self.list_is_lexically_ordered == other.list_is_lexically_ordered
505 }
506}
507
508pub fn uri_to_url(uri: &str) -> Result<Url> {
529 match Url::parse(uri) {
530 Ok(url) if url.scheme().len() == 1 && cfg!(windows) => {
531 local_path_to_url(uri)
533 }
534 Ok(url) => Ok(url),
535 Err(_) => local_path_to_url(uri),
536 }
537}
538
539fn expand_path(str_path: impl AsRef<str>) -> Result<std::path::PathBuf> {
540 let str_path = str_path.as_ref();
541 let expanded = expand_tilde_path(str_path).unwrap_or_else(|| str_path.into());
542
543 let mut expanded_path = path_abs::PathAbs::new(expanded)
544 .map_err(|err| {
545 Error::invalid_input(format!(
546 "Invalid table location: '{str_path}' could not be resolved to an absolute path: {err}"
547 ))
548 })?
549 .as_path()
550 .to_path_buf();
551 if let Some(s) = expanded_path.as_path().to_str()
553 && s.is_empty()
554 {
555 expanded_path = std::env::current_dir()?;
556 }
557
558 Ok(expanded_path)
559}
560
561fn expand_tilde_path(path: &str) -> Option<std::path::PathBuf> {
562 let home_dir = std::env::home_dir()?;
563 if path == "~" {
564 return Some(home_dir);
565 }
566 if let Some(stripped) = path.strip_prefix("~/") {
567 return Some(home_dir.join(stripped));
568 }
569 #[cfg(windows)]
570 if let Some(stripped) = path.strip_prefix("~\\") {
571 return Some(home_dir.join(stripped));
572 }
573
574 None
575}
576
577fn local_path_to_url(str_path: &str) -> Result<Url> {
578 let expanded_path = expand_path(str_path)?;
579
580 Url::from_directory_path(expanded_path).map_err(|_| {
581 Error::invalid_input_source(format!("Invalid table location: '{}'", str_path).into())
582 })
583}
584
585#[cfg(feature = "huggingface")]
586fn parse_hf_repo_id(url: &Url) -> Result<String> {
587 let mut segments: Vec<String> = Vec::new();
589 if let Some(host) = url.host_str() {
590 segments.push(host.to_string());
591 }
592 segments.extend(
593 url.path()
594 .trim_start_matches('/')
595 .split('/')
596 .map(|s| s.to_string()),
597 );
598
599 if segments.len() < 2 {
600 return Err(Error::invalid_input(
601 "Huggingface URL must contain at least owner and repo",
602 ));
603 }
604
605 let repo_type_candidates = ["models", "datasets", "spaces"];
606 let (owner, repo_with_rev) = if repo_type_candidates.contains(&segments[0].as_str()) {
607 if segments.len() < 3 {
608 return Err(Error::invalid_input(
609 "Huggingface URL missing owner/repo after repo type",
610 ));
611 }
612 (segments[1].as_str(), segments[2].as_str())
613 } else {
614 (segments[0].as_str(), segments[1].as_str())
615 };
616
617 let repo = repo_with_rev
618 .split_once('@')
619 .map(|(r, _)| r)
620 .unwrap_or(repo_with_rev);
621 Ok(format!("{owner}/{repo}"))
622}
623
624impl ObjectStore {
625 pub async fn from_uri(uri: &str) -> Result<(Arc<Self>, Path)> {
633 let registry = Arc::new(ObjectStoreRegistry::default());
634
635 Self::from_uri_and_params(registry, uri, &ObjectStoreParams::default()).await
636 }
637
638 pub async fn from_uri_and_params(
642 registry: Arc<ObjectStoreRegistry>,
643 uri: &str,
644 params: &ObjectStoreParams,
645 ) -> Result<(Arc<Self>, Path)> {
646 Self::from_uri_and_params_impl(registry, uri, params, true).await
647 }
648
649 #[doc(hidden)]
654 pub async fn from_uri_and_params_uncached(
655 registry: Arc<ObjectStoreRegistry>,
656 uri: &str,
657 params: &ObjectStoreParams,
658 ) -> Result<(Arc<Self>, Path)> {
659 Self::from_uri_and_params_impl(registry, uri, params, false).await
660 }
661
662 async fn from_uri_and_params_impl(
663 registry: Arc<ObjectStoreRegistry>,
664 uri: &str,
665 params: &ObjectStoreParams,
666 use_registry_cache: bool,
667 ) -> Result<(Arc<Self>, Path)> {
668 #[allow(deprecated)]
669 if let Some((store, path)) = params.object_store.as_ref() {
670 let mut inner = store.clone();
671 let store_prefix =
672 registry.calculate_object_store_prefix(uri, params.storage_options())?;
673
674 let mut io_tracker = IOTracker::default();
675 meter_store(
676 &mut inner,
677 &mut io_tracker,
678 &metrics_base(&store_prefix, path),
679 );
680
681 if let Some(wrapper) = params.object_store_wrapper.as_ref() {
682 inner = wrapper.wrap(&store_prefix, inner);
683 }
684
685 let tracked_store = io_tracker.wrap("", inner);
687
688 let store = Self {
689 inner: tracked_store,
690 local_dir_operations: None,
691 scheme: path.scheme().to_string(),
692 block_size: params.resolved_block_size()?.unwrap_or(64 * 1024),
693 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
694 use_constant_size_upload_parts: params.use_constant_size_upload_parts,
695 list_is_lexically_ordered: params.list_is_lexically_ordered.unwrap_or_default(),
696 io_parallelism: DEFAULT_CLOUD_IO_PARALLELISM,
697 download_retry_count: DEFAULT_DOWNLOAD_RETRY_COUNT,
698 io_tracker,
699 store_prefix,
700 paginated_lister: None,
702 };
703 let path = Path::parse(path.path())?;
704 return Ok((Arc::new(store), path));
705 }
706 let url = uri_to_url(uri)?;
707
708 let store = if use_registry_cache {
709 registry.get_store(url.clone(), params).await?
710 } else {
711 registry.new_store(url.clone(), params).await?
712 };
713 let provider = registry.get_provider(url.scheme()).expect_ok()?;
715 let path = provider.extract_path(&url)?;
716
717 Ok((store, path))
718 }
719
720 pub fn extract_path_from_uri(registry: Arc<ObjectStoreRegistry>, uri: &str) -> Result<Path> {
734 let url = uri_to_url(uri)?;
735 let provider = registry
736 .get_provider(url.scheme())
737 .ok_or_else(|| Error::invalid_input(format!("Unknown scheme: {}", url.scheme())))?;
738 provider.extract_path(&url)
739 }
740
741 #[deprecated(note = "Use `from_uri` instead")]
742 pub fn from_path(str_path: &str) -> Result<(Arc<Self>, Path)> {
743 Self::from_uri_and_params(
744 Arc::new(ObjectStoreRegistry::default()),
745 str_path,
746 &Default::default(),
747 )
748 .now_or_never()
749 .unwrap()
750 }
751
752 pub fn local() -> Self {
754 let provider = FileStoreProvider;
755 provider
756 .new_store(Url::parse("file:///").unwrap(), &Default::default())
757 .now_or_never()
758 .unwrap()
759 .unwrap()
760 }
761
762 pub fn memory() -> Self {
764 let provider = MemoryStoreProvider;
765 provider
766 .new_store(Url::parse("memory:///").unwrap(), &Default::default())
767 .now_or_never()
768 .unwrap()
769 .unwrap()
770 }
771
772 pub fn is_local(&self) -> bool {
774 self.scheme == "file" || self.scheme == "file+uring"
775 }
776
777 pub fn has_direct_local_paths(&self) -> bool {
782 self.is_local() && self.store_prefix == self.scheme
783 }
784
785 pub fn is_cloud(&self) -> bool {
786 if self.is_local() || self.scheme == "memory" || self.scheme == "shared-memory" {
787 return false;
788 }
789 true
790 }
791
792 pub fn prefers_lite_scheduler(&self) -> bool {
797 self.scheme == "file+uring"
798 }
799
800 pub fn scheme(&self) -> &str {
801 &self.scheme
802 }
803
804 pub fn block_size(&self) -> usize {
805 self.block_size
806 }
807
808 pub fn upload_memory_bound_bytes(&self, total_bytes: u64) -> u64 {
811 crate::object_writer::upload_memory_bound_bytes(
812 total_bytes,
813 self.use_constant_size_upload_parts,
814 )
815 }
816
817 pub fn max_iop_size(&self) -> u64 {
818 self.max_iop_size
819 }
820
821 pub fn io_parallelism(&self) -> usize {
828 std::env::var("LANCE_IO_THREADS")
829 .map(|val| val.parse::<usize>().unwrap())
830 .unwrap_or(self.io_parallelism)
831 .max(1)
832 }
833
834 pub fn io_tracker(&self) -> &IOTracker {
839 &self.io_tracker
840 }
841
842 pub fn io_stats_snapshot(&self) -> IoStats {
847 self.io_tracker.stats()
848 }
849
850 pub fn io_stats_incremental(&self) -> IoStats {
856 self.io_tracker.incremental_stats()
857 }
858
859 pub fn apply_wrapper(&mut self, wrapper: &dyn WrappingObjectStore) {
865 self.inner = wrapper.wrap(&self.store_prefix, self.inner.clone());
866 self.paginated_lister = self
867 .paginated_lister
868 .take()
869 .and_then(|lister| wrapper.wrap_paginated(&self.store_prefix, lister));
870 }
871
872 pub async fn open(&self, path: &Path) -> Result<Box<dyn Reader>> {
877 match self.scheme.as_str() {
878 "file" if self.has_direct_local_paths() => {
879 LocalObjectReader::open_with_tracker(
880 path,
881 self.block_size,
882 None,
883 Arc::new(self.io_tracker.clone()),
884 )
885 .await
886 }
887 #[cfg(target_os = "linux")]
888 "file+uring" => {
889 let use_current_thread = std::env::var("LANCE_URING_CURRENT_THREAD")
891 .map(|v| str_is_truthy(&v))
892 .unwrap_or(false);
893
894 if use_current_thread {
895 UringCurrentThreadReader::open(
896 path,
897 self.block_size,
898 None,
899 Arc::new(self.io_tracker.clone()),
900 )
901 .await
902 } else {
903 UringReader::open(
904 path,
905 self.block_size,
906 None,
907 Arc::new(self.io_tracker.clone()),
908 )
909 .await
910 }
911 }
912 _ => Ok(Box::new(
913 CloudObjectReader::new(
914 self.inner.clone(),
915 path.clone(),
916 self.block_size,
917 None,
918 self.download_retry_count,
919 )?
920 .with_io_parallelism(self.io_parallelism()),
921 )),
922 }
923 }
924
925 pub async fn open_with_size(&self, path: &Path, known_size: usize) -> Result<Box<dyn Reader>> {
931 if known_size <= self.block_size {
934 return Ok(Box::new(SmallReader::new(
935 self.inner.clone(),
936 path.clone(),
937 self.download_retry_count,
938 known_size,
939 )));
940 }
941
942 match self.scheme.as_str() {
943 "file" if self.has_direct_local_paths() => {
944 LocalObjectReader::open_with_tracker(
945 path,
946 self.block_size,
947 Some(known_size),
948 Arc::new(self.io_tracker.clone()),
949 )
950 .await
951 }
952 #[cfg(target_os = "linux")]
953 "file+uring" => {
954 let use_current_thread = std::env::var("LANCE_URING_CURRENT_THREAD")
956 .map(|v| str_is_truthy(&v))
957 .unwrap_or(false);
958
959 if use_current_thread {
960 UringCurrentThreadReader::open(
961 path,
962 self.block_size,
963 Some(known_size),
964 Arc::new(self.io_tracker.clone()),
965 )
966 .await
967 } else {
968 UringReader::open(
969 path,
970 self.block_size,
971 Some(known_size),
972 Arc::new(self.io_tracker.clone()),
973 )
974 .await
975 }
976 }
977 _ => Ok(Box::new(
978 CloudObjectReader::new(
979 self.inner.clone(),
980 path.clone(),
981 self.block_size,
982 Some(known_size),
983 self.download_retry_count,
984 )?
985 .with_io_parallelism(self.io_parallelism()),
986 )),
987 }
988 }
989
990 pub async fn create_local_writer(path: &std::path::Path) -> Result<ObjectWriter> {
992 let object_store = Self::local();
993 let absolute_path = expand_path(path.to_string_lossy())?;
994 let os_path = Path::from_absolute_path(absolute_path)?;
995 ObjectWriter::new(&object_store, &os_path).await
996 }
997
998 pub async fn open_local(path: &std::path::Path) -> Result<Box<dyn Reader>> {
1000 let object_store = Self::local();
1001 let absolute_path = expand_path(path.to_string_lossy())?;
1002 let os_path = Path::from_absolute_path(absolute_path)?;
1003 object_store.open(&os_path).await
1004 }
1005
1006 pub async fn create(&self, path: &Path) -> Result<Box<dyn Writer>> {
1008 match self.scheme.as_str() {
1009 "file" if self.has_direct_local_paths() => {
1010 let local_path = super::local::to_local_path(path);
1011 let local_path = std::path::PathBuf::from(&local_path);
1012 if let Some(parent) = local_path.parent() {
1013 tokio::fs::create_dir_all(parent).await?;
1014 }
1015 let parent = local_path
1016 .parent()
1017 .expect("file path must have parent")
1018 .to_owned();
1019 let named_temp = tokio::task::spawn_blocking(move || {
1020 #[cfg(unix)]
1021 {
1022 tempfile::Builder::new()
1024 .permissions(std::fs::Permissions::from_mode(0o666))
1025 .tempfile_in(parent)
1026 }
1027 #[cfg(not(unix))]
1028 tempfile::NamedTempFile::new_in(parent)
1029 })
1030 .await
1031 .map_err(|e| Error::io(format!("spawn_blocking failed: {}", e)))??;
1032 let (std_file, temp_path) = named_temp.into_parts();
1033 let file = tokio::fs::File::from_std(std_file);
1034 Ok(Box::new(LocalWriter::new(
1035 file,
1036 path.clone(),
1037 temp_path,
1038 Arc::new(self.io_tracker.clone()),
1039 )))
1040 }
1041 _ => Ok(Box::new(ObjectWriter::new(self, path).await?)),
1042 }
1043 }
1044
1045 pub async fn put(&self, path: &Path, content: &[u8]) -> Result<WriteResult> {
1047 let mut writer = self.create(path).await?;
1048 writer.write_all(content).await?;
1049 Writer::shutdown(writer.as_mut()).await
1050 }
1051
1052 pub async fn put_if_absent(
1061 &self,
1062 path: &Path,
1063 content: PutPayload,
1064 ) -> object_store::Result<()> {
1065 if self.scheme == "cos" {
1066 return Err(object_store::Error::NotSupported {
1067 source: "Tencent COS does not reliably enforce put-if-absent after bucket \
1068 versioning has ever been enabled"
1069 .into(),
1070 });
1071 }
1072
1073 if self.is_local() {
1074 let staging_path =
1075 Path::from(format!("{}.tmp.{}", path, uuid::Uuid::new_v4().simple()));
1076 self.inner.put(&staging_path, content).await?;
1077 let result = self.inner.rename_if_not_exists(&staging_path, path).await;
1078 if result.is_err()
1079 && let Err(error) = self.inner.delete(&staging_path).await
1080 {
1081 log::warn!(
1082 "Failed to remove staging object {} after atomic create failed: {}",
1083 staging_path,
1084 error
1085 );
1086 }
1087 result
1088 } else {
1089 self.inner
1090 .put_opts(
1091 path,
1092 content,
1093 PutOptions {
1094 mode: PutMode::Create,
1095 ..Default::default()
1096 },
1097 )
1098 .await
1099 .map(|_| ())
1100 }
1101 }
1102
1103 pub async fn delete(&self, path: &Path) -> Result<()> {
1104 self.inner.delete(path).await?;
1105 Ok(())
1106 }
1107
1108 const MAX_SINGLE_COPY_BYTES: u64 = 5 * 1024 * 1024 * 1024; pub async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
1113 let multipart_copy_fallback = matches!(self.scheme.as_str(), "s3" | "s3+ddb" | "gs");
1119 self.copy_impl(
1120 from,
1121 to,
1122 multipart_copy_fallback,
1123 Self::MAX_SINGLE_COPY_BYTES,
1124 )
1125 .await
1126 }
1127
1128 pub async fn copy_bulk(
1152 &self,
1153 source_path: &Path,
1154 destination_store: &Self,
1155 destination_path: &Path,
1156 ) -> Result<WriteResult> {
1157 self.copy_bulk_with_server_side_copy(
1158 source_path,
1159 destination_store,
1160 destination_path,
1161 self.uses_server_side_copy(destination_store),
1162 )
1163 .await
1164 }
1165
1166 fn uses_server_side_copy(&self, destination_store: &Self) -> bool {
1167 parse_env_as_bool(SERVER_SIDE_COPY_ENABLED_ENV, false)
1168 && self.can_server_side_copy_to(destination_store)
1169 }
1170
1171 async fn copy_bulk_with_server_side_copy(
1172 &self,
1173 source_path: &Path,
1174 destination_store: &Self,
1175 destination_path: &Path,
1176 server_side_copy_enabled: bool,
1177 ) -> Result<WriteResult> {
1178 if !server_side_copy_enabled || !self.can_server_side_copy_to(destination_store) {
1179 return self
1180 .copy_via_stream(source_path, destination_store, destination_path)
1181 .await;
1182 }
1183
1184 let source_size = self.size(source_path).await?;
1185 let result_size = usize::try_from(source_size).map_err(|source| {
1186 Error::io(format!(
1187 "server-side copy source size conversion failed from {source_path} to \
1188 {destination_path}: source_size={source_size}, error={source}"
1189 ))
1190 })?;
1191 destination_store
1192 .copy(source_path, destination_path)
1193 .await?;
1194 let destination_size = destination_store.size(destination_path).await?;
1195 if destination_size != source_size {
1196 return Err(Error::io(format!(
1197 "server-side copy destination size mismatch from {source_path} to \
1198 {destination_path}: source_size={source_size}, \
1199 destination_size={destination_size}"
1200 )));
1201 }
1202
1203 Ok(WriteResult {
1204 size: result_size,
1205 e_tag: None,
1206 })
1207 }
1208
1209 fn can_server_side_copy_to(&self, destination_store: &Self) -> bool {
1210 self.is_cloud()
1213 && destination_store.is_cloud()
1214 && Arc::ptr_eq(&self.inner, &destination_store.inner)
1215 }
1216
1217 #[instrument(
1240 name = "multipart_stream_copy",
1241 level = "info",
1242 skip(self, source_path, destination_store, destination_path),
1243 fields(
1244 source = %source_path,
1245 destination = %destination_path,
1246 source_size = Empty,
1247 read_chunk_size = Empty,
1248 multipart_part_size = crate::object_writer::initial_upload_size(),
1249 multipart_concurrency = crate::object_writer::max_upload_parallelism(),
1250 part_count = Empty,
1251 bytes_transferred = Empty,
1252 destination_size = Empty,
1253 validation = Empty,
1254 elapsed_ms = Empty,
1255 ),
1256 err
1257 )]
1258 pub async fn copy_via_stream(
1259 &self,
1260 source_path: &Path,
1261 destination_store: &Self,
1262 destination_path: &Path,
1263 ) -> Result<WriteResult> {
1264 self.copy_via_stream_inner(source_path, destination_store, destination_path)
1265 .await
1266 }
1267
1268 pub async fn copy_if_not_exists(
1273 &self,
1274 source_path: &Path,
1275 destination_path: &Path,
1276 ) -> Result<WriteResult> {
1277 let source_size = self.size(source_path).await?;
1278 match self
1279 .inner
1280 .copy_if_not_exists(source_path, destination_path)
1281 .await
1282 {
1283 Ok(()) => {}
1284 Err(
1285 object_store::Error::AlreadyExists { .. }
1286 | object_store::Error::Precondition { .. },
1287 ) => {
1288 let destination_size = self.size(destination_path).await?;
1289 if source_size != destination_size {
1290 return Err(Error::io(format!(
1291 "destination {destination_path} already exists with size \
1292 {destination_size}, but source {source_path} has size {source_size}"
1293 )));
1294 }
1295 }
1296 Err(error) => return Err(Error::io(error.to_string())),
1297 }
1298
1299 Ok(WriteResult {
1300 size: usize::try_from(source_size).map_err(|_| {
1301 Error::io(format!(
1302 "source {source_path} size {source_size} does not fit in usize"
1303 ))
1304 })?,
1305 e_tag: None,
1306 })
1307 }
1308
1309 async fn copy_via_stream_inner(
1310 &self,
1311 source_path: &Path,
1312 destination_store: &Self,
1313 destination_path: &Path,
1314 ) -> Result<WriteResult> {
1315 let started_at = Instant::now();
1316 if self.has_direct_local_paths() && destination_store.has_direct_local_paths() {
1317 let source_size = std::fs::metadata(super::local::to_local_path(source_path))
1318 .map_err(|source| {
1319 let source = if source.kind() == std::io::ErrorKind::NotFound {
1320 Error::not_found(source_path.to_string())
1321 } else {
1322 Error::from(source)
1323 };
1324 stream_copy_error("source metadata", source_path, destination_path, source)
1325 })?
1326 .len();
1327 let source_size = usize::try_from(source_size).map_err(|source| {
1328 stream_copy_error(
1329 "source size conversion",
1330 source_path,
1331 destination_path,
1332 source,
1333 )
1334 })?;
1335 Span::current().record("source_size", source_size as u64);
1336
1337 let metrics = destination_store.io_tracker.begin_io("copy");
1338 let result = super::local::copy_file(source_path, destination_path);
1339 metrics.record(&result, source_size as u64);
1340 result.map_err(|source| {
1341 stream_copy_error(
1342 "local filesystem copy",
1343 source_path,
1344 destination_path,
1345 source,
1346 )
1347 })?;
1348
1349 let destination_size =
1350 destination_store
1351 .size(destination_path)
1352 .await
1353 .map_err(|source| {
1354 stream_copy_error(
1355 "destination validation",
1356 source_path,
1357 destination_path,
1358 source,
1359 )
1360 })?;
1361 Span::current().record("bytes_transferred", source_size as u64);
1362 Span::current().record("destination_size", destination_size);
1363 if destination_size != source_size as u64 {
1364 Span::current().record("validation", "failed");
1365 return Err(Error::io(format!(
1366 "multipart_stream_copy destination size mismatch from {source_path} to \
1367 {destination_path}: source_size={source_size}, \
1368 destination_size={destination_size}"
1369 )));
1370 }
1371
1372 Span::current().record("validation", "passed");
1373 Span::current().record("elapsed_ms", started_at.elapsed().as_millis() as u64);
1374 return Ok(WriteResult {
1375 size: source_size,
1376 e_tag: None,
1377 });
1378 }
1379
1380 let reader = self.open(source_path).await.map_err(|source| {
1381 stream_copy_error("source open", source_path, destination_path, source)
1382 })?;
1383 let source_size = reader.size().await.map_err(|source| {
1384 stream_copy_error("source metadata", source_path, destination_path, source)
1385 })?;
1386 Span::current().record("source_size", source_size as u64);
1387
1388 let mut writer = destination_store
1389 .create(destination_path)
1390 .await
1391 .map_err(|source| {
1392 stream_copy_error(
1393 "destination writer creation",
1394 source_path,
1395 destination_path,
1396 source,
1397 )
1398 })?;
1399 let read_chunk_size = usize::try_from(self.max_iop_size())
1400 .unwrap_or(usize::MAX)
1401 .max(1);
1402 Span::current().record("read_chunk_size", read_chunk_size as u64);
1403 let mut bytes_transferred = 0usize;
1404 if source_size > 0 {
1405 let first_range = 0..read_chunk_size.min(source_size);
1406 let mut current_range = first_range.clone();
1407 let mut current_bytes = reader.get_range(first_range).await.map_err(|source| {
1408 stream_copy_error("source read", source_path, destination_path, source)
1409 })?;
1410
1411 loop {
1412 let expected_bytes = current_range.len();
1413 if current_bytes.len() != expected_bytes {
1414 Span::current().record("validation", "failed");
1415 return Err(Error::io(format!(
1416 "multipart_stream_copy source range size mismatch from {source_path} to \
1417 {destination_path}: range={current_range:?}, \
1418 expected_bytes={expected_bytes}, actual_bytes={}",
1419 current_bytes.len()
1420 )));
1421 }
1422 bytes_transferred = bytes_transferred
1423 .checked_add(current_bytes.len())
1424 .ok_or_else(|| {
1425 Error::io(format!(
1426 "multipart_stream_copy byte count overflow from {source_path} to \
1427 {destination_path}"
1428 ))
1429 })?;
1430
1431 if bytes_transferred == source_size {
1432 writer.write_all(¤t_bytes).await.map_err(|source| {
1433 stream_copy_error(
1434 "destination write",
1435 source_path,
1436 destination_path,
1437 source,
1438 )
1439 })?;
1440 break;
1441 }
1442
1443 let range_end = bytes_transferred
1444 .checked_add(read_chunk_size)
1445 .unwrap_or(source_size)
1446 .min(source_size);
1447 let next_range = bytes_transferred..range_end;
1448 let next_read = reader.get_range(next_range.clone());
1449 let (write_result, next_bytes) =
1450 tokio::join!(writer.write_all(¤t_bytes), next_read);
1451 write_result.map_err(|source| {
1452 stream_copy_error("destination write", source_path, destination_path, source)
1453 })?;
1454 current_bytes = next_bytes.map_err(|source| {
1455 stream_copy_error("source read", source_path, destination_path, source)
1456 })?;
1457 current_range = next_range;
1458 }
1459 }
1460 Span::current().record("bytes_transferred", bytes_transferred as u64);
1461
1462 let write_result = Writer::shutdown(writer.as_mut()).await.map_err(|source| {
1463 stream_copy_error(
1464 "destination completion",
1465 source_path,
1466 destination_path,
1467 source,
1468 )
1469 })?;
1470 if write_result.size != source_size {
1471 Span::current().record("validation", "failed");
1472 return Err(Error::io(format!(
1473 "multipart_stream_copy writer size mismatch from {source_path} to \
1474 {destination_path}: source_size={source_size}, \
1475 writer_size={}",
1476 write_result.size
1477 )));
1478 }
1479
1480 let destination_size =
1481 destination_store
1482 .size(destination_path)
1483 .await
1484 .map_err(|source| {
1485 stream_copy_error(
1486 "destination validation",
1487 source_path,
1488 destination_path,
1489 source,
1490 )
1491 })?;
1492 Span::current().record("destination_size", destination_size);
1493 if destination_size != source_size as u64 {
1494 Span::current().record("validation", "failed");
1495 return Err(Error::io(format!(
1496 "multipart_stream_copy destination size mismatch from {source_path} to \
1497 {destination_path}: source_size={source_size}, \
1498 destination_size={destination_size}"
1499 )));
1500 }
1501
1502 Span::current().record("validation", "passed");
1503 Span::current().record("elapsed_ms", started_at.elapsed().as_millis() as u64);
1504 Ok(write_result)
1505 }
1506
1507 async fn copy_impl(
1513 &self,
1514 from: &Path,
1515 to: &Path,
1516 multipart_copy_fallback: bool,
1517 max_single_copy: u64,
1518 ) -> Result<()> {
1519 if self.has_direct_local_paths() {
1520 let metrics = self.io_tracker.begin_io("copy");
1522 let result = super::local::copy_file(from, to);
1523 metrics.record(&result, 0);
1524 return result;
1525 }
1526 if multipart_copy_fallback {
1527 let reader = self.open(from).await?;
1530 if reader.size().await? as u64 > max_single_copy {
1531 let mut writer = self.create(to).await?;
1532 writer.copy_from_reader(reader.as_ref()).await?;
1533 Writer::shutdown(writer.as_mut()).await?;
1534 return Ok(());
1535 }
1536 }
1537 Ok(self.inner.copy(from, to).await?)
1538 }
1539
1540 pub async fn read_dir(&self, dir_path: impl Into<Path>) -> Result<Vec<String>> {
1545 let path = dir_path.into();
1546 let path = Path::parse(&path)?;
1547 let output = self.inner.list_with_delimiter(Some(&path)).await?;
1548 Ok(output
1549 .common_prefixes
1550 .iter()
1551 .chain(output.objects.iter().map(|o| &o.location))
1552 .filter_map(|s| s.filename().map(|f| f.to_string()))
1553 .collect())
1554 }
1555
1556 pub async fn list_with_delimiter(&self, prefix: Option<&Path>) -> Result<ListResult> {
1563 Ok(self.inner.list_with_delimiter(prefix).await?)
1564 }
1565
1566 pub fn list(
1570 &self,
1571 path: Option<Path>,
1572 ) -> Pin<Box<dyn Stream<Item = Result<ObjectMeta>> + Send>> {
1573 Box::pin(
1574 ListRetryStream::new(self.inner.clone(), path, 5)
1575 .with_lexical_ordering(self.list_is_lexically_ordered)
1576 .map(|m| m.map_err(|e| e.into())),
1577 )
1578 }
1579
1580 pub fn read_dir_all<'a, 'b>(
1584 &'a self,
1585 dir_path: impl Into<&'b Path> + Send,
1586 unmodified_since: Option<DateTime<Utc>>,
1587 ) -> BoxStream<'a, Result<ObjectMeta>> {
1588 self.inner.read_dir_all(dir_path, unmodified_since)
1589 }
1590
1591 pub async fn remove_dir_all(&self, dir_path: impl Into<Path>) -> Result<()> {
1593 let path = dir_path.into();
1594 let path = Path::parse(&path)?;
1595
1596 if let Some(local_dir_operations) = &self.local_dir_operations {
1597 let metrics = self.io_tracker.begin_io("delete");
1598 let result = local_dir_operations.remove_dir_all(&path).await;
1599 metrics.record(&result, 0);
1600 return result;
1601 }
1602 if self.has_direct_local_paths() {
1603 let metrics = self.io_tracker.begin_io("delete");
1607 let result = super::local::remove_dir_all(&path);
1608 metrics.record(&result, 0);
1609 return result;
1610 }
1611 let sub_entries = self
1612 .inner
1613 .list(Some(&path))
1614 .map(|m| m.map(|meta| meta.location))
1615 .boxed();
1616 self.inner
1617 .delete_stream(sub_entries)
1618 .try_collect::<Vec<_>>()
1619 .await?;
1620 if self.scheme == "file-object-store" {
1621 return super::local::remove_dir_all(&path);
1624 }
1625 Ok(())
1626 }
1627
1628 pub async fn remove_empty_dirs(
1654 &self,
1655 root_path: impl Into<Path>,
1656 retained_dirs: HashSet<Path>,
1657 verified_dirs: HashSet<Path>,
1658 unmodified_since: Option<DateTime<Utc>>,
1659 ) -> Result<()> {
1660 if !self.has_direct_local_paths() && self.scheme != "file-object-store" {
1661 return Ok(());
1662 }
1663
1664 let path = Path::parse(root_path.into())?;
1665 let metrics = self.io_tracker.begin_io("delete");
1666 let result = tokio::task::spawn_blocking(move || {
1667 super::local::remove_empty_dirs(&path, &retained_dirs, &verified_dirs, unmodified_since)
1668 })
1669 .await
1670 .map_err(|error| Error::io(format!("empty-directory cleanup task failed: {error}")))?;
1671 metrics.record(&result, 0);
1672 result
1673 }
1674
1675 pub fn remove_stream<'a>(
1690 &'a self,
1691 locations: BoxStream<'a, Result<Path>>,
1692 ) -> BoxStream<'a, Result<Path>> {
1693 let store = Arc::clone(&self.inner);
1694 locations
1695 .map(move |location| {
1696 let store = Arc::clone(&store);
1697 async move {
1698 let location = location?;
1699 match store.delete(&location).await {
1700 Ok(()) => Ok(location),
1701 Err(object_store::Error::NotFound { .. }) => Ok(location),
1702 Err(error) => Err(error.into()),
1703 }
1704 }
1705 })
1706 .buffer_unordered(self.io_parallelism())
1707 .boxed()
1708 }
1709
1710 pub async fn exists(&self, path: &Path) -> Result<bool> {
1712 match self.inner.head(path).await {
1713 Ok(_) => Ok(true),
1714 Err(object_store::Error::NotFound { path: _, source: _ }) => Ok(false),
1715 Err(e) => Err(e.into()),
1716 }
1717 }
1718
1719 pub async fn size(&self, path: &Path) -> Result<u64> {
1721 Ok(self.inner.head(path).await?.size)
1722 }
1723
1724 pub async fn read_one_all(&self, path: &Path) -> Result<Bytes> {
1726 let reader = self.open(path).await?;
1727 Ok(reader.get_all().await?)
1728 }
1729
1730 pub async fn read_one_range(&self, path: &Path, range: Range<usize>) -> Result<Bytes> {
1735 let reader = self.open(path).await?;
1736 Ok(reader.get_range(range).await?)
1737 }
1738}
1739
1740#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy)]
1742pub enum LanceConfigKey {
1743 DownloadRetryCount,
1745}
1746
1747impl FromStr for LanceConfigKey {
1748 type Err = Error;
1749
1750 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
1751 match s.to_ascii_lowercase().as_str() {
1752 "download_retry_count" => Ok(Self::DownloadRetryCount),
1753 _ => Err(Error::invalid_input_source(
1754 format!("Invalid LanceConfigKey: {}", s).into(),
1755 )),
1756 }
1757 }
1758}
1759
1760#[derive(Clone, Debug, Default)]
1761pub struct StorageOptions(pub HashMap<String, String>);
1762
1763impl StorageOptions {
1764 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1765 fn as_cloud_options<K>(&self) -> HashMap<K, String>
1766 where
1767 K: FromStr + AsRef<str> + Eq + std::hash::Hash,
1768 {
1769 let mut options: HashMap<K, (&str, &str)> = HashMap::new();
1770 for (source_key, value) in &self.0 {
1771 let Ok(config_key) = K::from_str(&source_key.to_ascii_lowercase()) else {
1772 continue;
1773 };
1774 let source_key = source_key.as_str();
1775 let canonical_key = config_key.as_ref();
1776 if let Some((selected_key, _)) = options.get(&config_key)
1778 && (
1779 !source_key.eq_ignore_ascii_case(canonical_key),
1780 source_key != canonical_key,
1781 source_key,
1782 ) >= (
1783 !selected_key.eq_ignore_ascii_case(canonical_key),
1784 *selected_key != canonical_key,
1785 *selected_key,
1786 )
1787 {
1788 continue;
1789 }
1790 options.insert(config_key, (source_key, value.as_str()));
1791 }
1792 options
1793 .into_iter()
1794 .map(|(key, (_, value))| (key, value.to_string()))
1795 .collect()
1796 }
1797
1798 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1799 fn merge_env_options(
1800 &mut self,
1801 env_options: impl IntoIterator<
1802 Item = (impl AsRef<std::ffi::OsStr>, impl AsRef<std::ffi::OsStr>),
1803 >,
1804 canonical_key: impl Fn(&str) -> Option<String>,
1805 ) {
1806 let mut configured_keys: HashSet<String> = self
1808 .0
1809 .keys()
1810 .filter_map(|key| canonical_key(&key.to_ascii_lowercase()))
1811 .collect();
1812 for (key, value) in env_options {
1813 if let (Some(key), Some(value)) = (key.as_ref().to_str(), value.as_ref().to_str())
1814 && let Some(config_key) = canonical_key(&key.to_ascii_lowercase())
1815 && configured_keys.insert(config_key.clone())
1816 {
1817 self.0.insert(config_key, value.to_string());
1818 }
1819 }
1820 }
1821
1822 pub fn new(options: HashMap<String, String>) -> Self {
1824 let mut options = options;
1825 if let Ok(value) = std::env::var("AZURE_STORAGE_ALLOW_HTTP") {
1826 options.insert("allow_http".into(), value);
1827 }
1828 if let Ok(value) = std::env::var("AZURE_STORAGE_USE_HTTP") {
1829 options.insert("allow_http".into(), value);
1830 }
1831 if let Ok(value) = std::env::var("AWS_ALLOW_HTTP") {
1832 options.insert("allow_http".into(), value);
1833 }
1834 if let Ok(value) = std::env::var("OBJECT_STORE_CLIENT_MAX_RETRIES") {
1835 options.insert("client_max_retries".into(), value);
1836 }
1837 if let Ok(value) = std::env::var("OBJECT_STORE_CLIENT_RETRY_TIMEOUT") {
1838 options.insert("client_retry_timeout".into(), value);
1839 }
1840 Self(options)
1841 }
1842
1843 pub fn allow_http(&self) -> bool {
1845 self.0.iter().any(|(key, value)| {
1846 key.to_ascii_lowercase().contains("allow_http") & str_is_truthy(value)
1847 })
1848 }
1849
1850 pub fn block_size(&self) -> Result<Option<usize>> {
1853 let Some((_, value)) = self
1854 .0
1855 .iter()
1856 .find(|(key, _)| key.eq_ignore_ascii_case("block_size"))
1857 else {
1858 return Ok(None);
1859 };
1860 let block_size = value.trim().parse::<usize>().map_err(|err| {
1861 Error::invalid_input(format!(
1862 "storage option block_size must be a number of bytes, got `{value}`: {err}"
1863 ))
1864 })?;
1865 Ok(Some(block_size))
1866 }
1867
1868 pub fn download_retry_count(&self) -> usize {
1870 self.0
1871 .iter()
1872 .find(|(key, _)| key.eq_ignore_ascii_case("download_retry_count"))
1873 .map(|(_, value)| value.parse::<usize>().unwrap_or(3))
1874 .unwrap_or(3)
1875 }
1876
1877 pub fn client_max_retries(&self) -> usize {
1879 self.0
1880 .iter()
1881 .find(|(key, _)| key.eq_ignore_ascii_case("client_max_retries"))
1882 .and_then(|(_, value)| value.parse::<usize>().ok())
1883 .unwrap_or(3)
1884 }
1885
1886 pub fn client_retry_timeout(&self) -> u64 {
1888 self.0
1889 .iter()
1890 .find(|(key, _)| key.eq_ignore_ascii_case("client_retry_timeout"))
1891 .and_then(|(_, value)| value.parse::<u64>().ok())
1892 .unwrap_or(180)
1893 }
1894
1895 pub fn get(&self, key: &str) -> Option<&String> {
1896 self.0.get(key)
1897 }
1898
1899 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1907 pub fn client_options(&self) -> Result<ClientOptions> {
1908 let mut headers = HeaderMap::new();
1909 for (key, value) in &self.0 {
1910 if let Some(header_name) = key.strip_prefix("headers.") {
1911 let name = header_name
1912 .parse::<http::header::HeaderName>()
1913 .map_err(|e| {
1914 Error::invalid_input(format!("invalid header name '{header_name}': {e}"))
1915 })?;
1916 let val = HeaderValue::from_str(value).map_err(|e| {
1917 Error::invalid_input(format!("invalid header value for '{header_name}': {e}"))
1918 })?;
1919 headers.insert(name, val);
1920 }
1921 }
1922 let mut client_options = ClientOptions::default();
1923 if !headers.is_empty() {
1924 client_options = client_options.with_default_headers(headers);
1925 }
1926 Ok(client_options)
1927 }
1928
1929 pub fn expires_at_millis(&self) -> Option<u64> {
1931 self.0
1932 .get(EXPIRES_AT_MILLIS_KEY)
1933 .and_then(|s| s.parse::<u64>().ok())
1934 }
1935}
1936
1937impl From<HashMap<String, String>> for StorageOptions {
1938 fn from(value: HashMap<String, String>) -> Self {
1939 Self::new(value)
1940 }
1941}
1942
1943static DEFAULT_OBJECT_STORE_REGISTRY: std::sync::LazyLock<ObjectStoreRegistry> =
1944 std::sync::LazyLock::new(ObjectStoreRegistry::default);
1945
1946impl ObjectStore {
1947 #[allow(clippy::too_many_arguments)]
1948 pub fn new(
1949 mut store: Arc<DynObjectStore>,
1950 location: Url,
1951 block_size: Option<usize>,
1952 wrapper: Option<Arc<dyn WrappingObjectStore>>,
1953 use_constant_size_upload_parts: bool,
1954 list_is_lexically_ordered: bool,
1955 io_parallelism: usize,
1956 download_retry_count: usize,
1957 storage_options: Option<&HashMap<String, String>>,
1958 ) -> Self {
1959 let scheme = location.scheme();
1960 let block_size = block_size.unwrap_or_else(|| infer_block_size(scheme));
1961 let store_prefix = match DEFAULT_OBJECT_STORE_REGISTRY.get_provider(scheme) {
1962 Some(provider) => provider
1963 .calculate_object_store_prefix(&location, storage_options)
1964 .unwrap(),
1965 None => {
1966 let store_prefix = format!("{}${}", location.scheme(), location.authority());
1967 log::warn!(
1968 "Guessing that object store prefix is {}, since object store scheme is not found in registry.",
1969 store_prefix
1970 );
1971 store_prefix
1972 }
1973 };
1974 let mut io_tracker = IOTracker::default();
1975 meter_store(
1976 &mut store,
1977 &mut io_tracker,
1978 &metrics_base(&store_prefix, &location),
1979 );
1980
1981 let store = match wrapper {
1982 Some(wrapper) => wrapper.wrap(&store_prefix, store),
1983 None => store,
1984 };
1985
1986 let tracked_store = io_tracker.wrap("", store);
1988
1989 Self {
1990 inner: tracked_store,
1991 local_dir_operations: None,
1992 scheme: scheme.into(),
1993 block_size,
1994 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
1995 use_constant_size_upload_parts,
1996 list_is_lexically_ordered,
1997 io_parallelism,
1998 download_retry_count,
1999 io_tracker,
2000 store_prefix,
2001 paginated_lister: None,
2003 }
2004 }
2005}
2006
2007#[cfg(feature = "metrics")]
2016fn meter_store(inner: &mut Arc<dyn OSObjectStore>, io_tracker: &mut IOTracker, base: &str) {
2017 use crate::object_store::metrics::ObjectStoreMetricsExt;
2018 io_tracker.set_metrics_base(base);
2019 *inner = inner.clone().metered(base.to_owned());
2020}
2021
2022#[cfg(not(feature = "metrics"))]
2023fn meter_store(_inner: &mut Arc<dyn OSObjectStore>, _io_tracker: &mut IOTracker, _base: &str) {}
2024
2025#[cfg(feature = "metrics")]
2029pub(crate) fn metrics_base(store_prefix: &str, location: &Url) -> String {
2030 metrics::metrics_base(metrics::base_label_mode(), store_prefix, location)
2031}
2032
2033#[cfg(not(feature = "metrics"))]
2034pub(crate) fn metrics_base(store_prefix: &str, _location: &Url) -> String {
2035 store_prefix.to_owned()
2036}
2037
2038fn infer_block_size(scheme: &str) -> usize {
2039 match scheme {
2043 "file" => 4 * 1024,
2044 _ => 64 * 1024,
2045 }
2046}
2047
2048#[cfg(test)]
2049mod tests {
2050 use super::*;
2051 use async_trait::async_trait;
2052 use bytes::Bytes;
2053 use lance_core::utils::tempfile::{TempStdDir, TempStdFile, TempStrDir};
2054 use object_store::memory::InMemory;
2055 use object_store::{
2056 CopyOptions, GetOptions, GetResult, ListResult, MultipartUpload, PutMultipartOptions,
2057 PutOptions, PutPayload, PutResult, Result as OSResult, UploadPart,
2058 };
2059 use rstest::rstest;
2060 use serial_test::serial;
2061 use std::env::set_current_dir;
2062 use std::fmt::{Display, Formatter};
2063 use std::fs::{create_dir_all, write};
2064 use std::ops::Range;
2065 use std::path::Path as StdPath;
2066 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
2067
2068 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
2069 fn canonical_test_config_key<K: FromStr + AsRef<str>>(key: &str) -> Option<String> {
2070 K::from_str(key).ok().map(|key| key.as_ref().to_string())
2071 }
2072
2073 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
2074 #[rstest]
2075 #[cfg_attr(feature = "aws", case::aws("region", "aws_region", canonical_test_config_key::<object_store::aws::AmazonS3ConfigKey>))]
2076 #[cfg_attr(feature = "azure", case::azure("account_name", "azure_storage_account_name", canonical_test_config_key::<object_store::azure::AzureConfigKey>))]
2077 #[cfg_attr(feature = "gcp", case::gcp("service_account_path", "google_service_account", canonical_test_config_key::<object_store::gcp::GoogleConfigKey>))]
2078 fn test_merge_env_options(
2079 #[case] alias: &str,
2080 #[case] canonical: &str,
2081 #[case] canonical_key: fn(&str) -> Option<String>,
2082 #[values(false, true)] use_alias: bool,
2083 #[values(false, true)] uppercase: bool,
2084 #[values(None, Some(""), Some("explicit"))] explicit_value: Option<&str>,
2085 ) {
2086 let key = if use_alias { alias } else { canonical };
2087 let key = if uppercase {
2088 key.to_ascii_uppercase()
2089 } else {
2090 key.to_string()
2091 };
2092 let mut expected = HashMap::from([("lance_custom_option".into(), "unchanged".into())]);
2093 if let Some(value) = explicit_value {
2094 expected.insert(key, value.to_string());
2095 }
2096 let mut options = StorageOptions(expected.clone());
2097 if explicit_value.is_none() {
2098 expected.insert(canonical.to_string(), "environment".to_string());
2099 }
2100 let env_options = [
2101 (canonical.to_ascii_uppercase(), "environment"),
2102 (alias.to_string(), "second_environment_alias"),
2103 ("lance_custom_option".to_string(), "ignored"),
2104 ("unrecognized_env_option".to_string(), "ignored"),
2105 ];
2106
2107 options.merge_env_options(
2108 env_options.iter().map(|(key, value)| (key, value)),
2109 canonical_key,
2110 );
2111 assert_eq!(options.0, expected);
2112 options.merge_env_options(
2113 env_options.iter().map(|(key, value)| (key, value)),
2114 canonical_key,
2115 );
2116 assert_eq!(options.0, expected);
2117 }
2118
2119 fn write_to_file(path_str: &str, contents: &str) -> std::io::Result<()> {
2121 let path = expand_path(path_str).map_err(std::io::Error::other)?;
2122 std::fs::create_dir_all(path.parent().unwrap())?;
2123 write(path, contents)
2124 }
2125
2126 async fn read_from_store(store: &ObjectStore, path: &Path) -> Result<String> {
2127 let test_file_store = store.open(path).await.unwrap();
2128 let size = test_file_store.size().await.unwrap();
2129 let bytes = test_file_store.get_range(0..size).await.unwrap();
2130 let contents = String::from_utf8(bytes.to_vec()).unwrap();
2131 Ok(contents)
2132 }
2133
2134 #[tokio::test]
2135 async fn test_put_if_absent() {
2136 let temp_dir = TempStrDir::default();
2137 let path = Path::from(format!("{}/atomic-create", temp_dir.as_str()));
2138 let store = ObjectStore::local();
2139 store
2140 .put_if_absent(&path, Bytes::from_static(b"first").into())
2141 .await
2142 .unwrap();
2143 let error = store
2144 .put_if_absent(&path, Bytes::from_static(b"second").into())
2145 .await
2146 .unwrap_err();
2147 assert!(matches!(
2148 error,
2149 object_store::Error::AlreadyExists { .. } | object_store::Error::Precondition { .. }
2150 ));
2151 assert_eq!(
2152 store.read_one_all(&path).await.unwrap(),
2153 b"first".as_slice()
2154 );
2155 }
2156
2157 #[tokio::test]
2158 async fn test_put_if_absent_rejects_cos() {
2159 let mut store = ObjectStore::memory();
2160 store.scheme = "cos".to_string();
2161 let path = Path::from("atomic-create");
2162
2163 let error = store
2164 .put_if_absent(&path, Bytes::from_static(b"value").into())
2165 .await
2166 .unwrap_err();
2167
2168 assert!(matches!(error, object_store::Error::NotSupported { .. }));
2169 assert!(!store.exists(&path).await.unwrap());
2170 }
2171
2172 #[tokio::test]
2173 async fn test_io_parallelism_clamped_to_nonzero() {
2174 let store = ObjectStore::local();
2177 let mem_store = ObjectStore::memory();
2180 let path = Path::from("/io_parallelism_probe");
2181 mem_store.put(&path, b"x").await.unwrap();
2182
2183 unsafe { std::env::set_var("LANCE_IO_THREADS", "0") };
2186 assert_eq!(
2187 store.io_parallelism(),
2188 1,
2189 "LANCE_IO_THREADS=0 must clamp to 1"
2190 );
2191 assert_eq!(
2192 mem_store.open(&path).await.unwrap().io_parallelism(),
2193 1,
2194 "an opened reader must report the store's clamped parallelism"
2195 );
2196
2197 unsafe { std::env::set_var("LANCE_IO_THREADS", "8") };
2198 assert_eq!(
2199 store.io_parallelism(),
2200 8,
2201 "a positive override must pass through unchanged"
2202 );
2203 assert_eq!(
2204 mem_store.open(&path).await.unwrap().io_parallelism(),
2205 8,
2206 "an opened reader must honor the configured request limit"
2207 );
2208 assert_eq!(
2209 mem_store
2210 .open_with_size(&path, 1024 * 1024)
2211 .await
2212 .unwrap()
2213 .io_parallelism(),
2214 8,
2215 "a sized reader must honor the configured request limit"
2216 );
2217
2218 unsafe { std::env::remove_var("LANCE_IO_THREADS") };
2219 assert!(
2220 store.io_parallelism() >= 1,
2221 "the configured default parallelism must be at least 1"
2222 );
2223 }
2224
2225 #[tokio::test]
2226 async fn test_absolute_paths() {
2227 let tmp_path = TempStrDir::default();
2228 write_to_file(
2229 &format!("{tmp_path}/bar/foo.lance/test_file"),
2230 "TEST_CONTENT",
2231 )
2232 .unwrap();
2233
2234 for uri in &[
2236 format!("{tmp_path}/bar/foo.lance"),
2237 format!("{tmp_path}/./bar/foo.lance"),
2238 format!("{tmp_path}/bar/foo.lance/../foo.lance"),
2239 ] {
2240 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
2241 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
2242 .await
2243 .unwrap();
2244 assert_eq!(contents, "TEST_CONTENT");
2245 }
2246 }
2247
2248 #[tokio::test]
2249 async fn test_cloud_paths() {
2250 let uri = "s3://bucket/foo.lance";
2251 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
2252 assert_eq!(store.scheme, "s3");
2253 assert_eq!(path.to_string(), "foo.lance");
2254
2255 let (store, path) = ObjectStore::from_uri("s3+ddb://bucket/foo.lance")
2256 .await
2257 .unwrap();
2258 assert_eq!(store.scheme, "s3");
2259 assert_eq!(path.to_string(), "foo.lance");
2260
2261 let (store, path) = ObjectStore::from_uri("gs://bucket/foo.lance")
2262 .await
2263 .unwrap();
2264 assert_eq!(store.scheme, "gs");
2265 assert_eq!(path.to_string(), "foo.lance");
2266
2267 let (store, path) =
2268 ObjectStore::from_uri("abfss://filesystem@account.dfs.core.windows.net/foo.lance")
2269 .await
2270 .unwrap();
2271 assert_eq!(store.scheme, "abfss");
2272 assert_eq!(path.to_string(), "foo.lance");
2273 }
2274
2275 async fn test_block_size_used_test_helper(
2276 uri: &str,
2277 storage_options: Option<HashMap<String, String>>,
2278 default_expected_block_size: usize,
2279 ) {
2280 let registry = Arc::new(ObjectStoreRegistry::default());
2282 let accessor = storage_options
2283 .clone()
2284 .map(|opts| Arc::new(StorageOptionsAccessor::with_static_options(opts)));
2285 let params = ObjectStoreParams {
2286 storage_options_accessor: accessor.clone(),
2287 ..ObjectStoreParams::default()
2288 };
2289 let (store, _) = ObjectStore::from_uri_and_params(registry, uri, ¶ms)
2290 .await
2291 .unwrap();
2292 assert_eq!(store.block_size, default_expected_block_size);
2293
2294 let registry = Arc::new(ObjectStoreRegistry::default());
2296 let params = ObjectStoreParams {
2297 block_size: Some(1024),
2298 storage_options_accessor: accessor,
2299 ..ObjectStoreParams::default()
2300 };
2301 let (store, _) = ObjectStore::from_uri_and_params(registry, uri, ¶ms)
2302 .await
2303 .unwrap();
2304 assert_eq!(store.block_size, 1024);
2305
2306 let mut options_with_block_size = storage_options.unwrap_or_default();
2308 options_with_block_size.insert(String::from("block_size"), String::from("2048"));
2309 let accessor = Arc::new(StorageOptionsAccessor::with_static_options(
2310 options_with_block_size,
2311 ));
2312 let registry = Arc::new(ObjectStoreRegistry::default());
2313 let params = ObjectStoreParams {
2314 storage_options_accessor: Some(accessor.clone()),
2315 ..ObjectStoreParams::default()
2316 };
2317 let (store, _) = ObjectStore::from_uri_and_params(registry, uri, ¶ms)
2318 .await
2319 .unwrap();
2320 assert_eq!(store.block_size, 2048);
2321
2322 let registry = Arc::new(ObjectStoreRegistry::default());
2324 let params = ObjectStoreParams {
2325 block_size: Some(1024),
2326 storage_options_accessor: Some(accessor),
2327 ..ObjectStoreParams::default()
2328 };
2329 let (store, _) = ObjectStore::from_uri_and_params(registry, uri, ¶ms)
2330 .await
2331 .unwrap();
2332 assert_eq!(store.block_size, 1024);
2333 }
2334
2335 #[tokio::test]
2336 async fn test_block_size_option_rejects_invalid_values() {
2337 let registry = Arc::new(ObjectStoreRegistry::default());
2338 let accessor = Arc::new(StorageOptionsAccessor::with_static_options(HashMap::from(
2339 [(String::from("block_size"), String::from("64KiB"))],
2340 )));
2341 let params = ObjectStoreParams {
2342 storage_options_accessor: Some(accessor),
2343 ..ObjectStoreParams::default()
2344 };
2345 let error =
2346 ObjectStore::from_uri_and_params(registry, "memory:///bucket/foo.lance", ¶ms)
2347 .await
2348 .unwrap_err();
2349 assert!(
2350 matches!(error, lance_core::Error::InvalidInput { .. }),
2351 "{error:?}"
2352 );
2353 assert!(error.to_string().contains("block_size"), "{error}");
2354 }
2355
2356 #[rstest]
2357 #[case("s3://bucket/foo.lance", None)]
2358 #[case("gs://bucket/foo.lance", None)]
2359 #[case("az://account/bucket/foo.lance",
2360 Some(HashMap::from([
2361 (String::from("account_name"), String::from("account")),
2362 (String::from("container_name"), String::from("container"))
2363 ])))]
2364 #[case("abfss://filesystem@account.dfs.core.windows.net/foo.lance",
2365 Some(HashMap::from([
2366 (String::from("account_name"), String::from("account")),
2367 (String::from("container_name"), String::from("filesystem"))
2368 ])))]
2369 #[tokio::test]
2370 async fn test_block_size_used_cloud(
2371 #[case] uri: &str,
2372 #[case] storage_options: Option<HashMap<String, String>>,
2373 ) {
2374 test_block_size_used_test_helper(uri, storage_options, 64 * 1024).await;
2375 }
2376
2377 #[rstest]
2378 #[case("file")]
2379 #[case("file-object-store")]
2380 #[case("memory:///bucket/foo.lance")]
2381 #[tokio::test]
2382 async fn test_block_size_used_file(#[case] prefix: &str) {
2383 let tmp_path = TempStrDir::default();
2384 let path = format!("{tmp_path}/bar/foo.lance/test_file");
2385 write_to_file(&path, "URL").unwrap();
2386 let uri = format!("{prefix}:///{path}");
2387 test_block_size_used_test_helper(&uri, None, 4 * 1024).await;
2388 }
2389
2390 #[tokio::test]
2391 async fn test_relative_paths() {
2392 let tmp_path = TempStrDir::default();
2393 write_to_file(
2394 &format!("{tmp_path}/bar/foo.lance/test_file"),
2395 "RELATIVE_URL",
2396 )
2397 .unwrap();
2398
2399 let original_dir = std::env::current_dir().unwrap();
2403 set_current_dir(StdPath::new(tmp_path.as_ref())).expect("Error changing current dir");
2404 let opened = ObjectStore::from_uri("./bar/foo.lance").await;
2405 set_current_dir(&original_dir).expect("Error restoring current dir");
2406 let (store, path) = opened.unwrap();
2407
2408 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
2409 .await
2410 .unwrap();
2411 assert_eq!(contents, "RELATIVE_URL");
2412 }
2413
2414 #[tokio::test]
2415 async fn test_tilde_expansion() {
2416 let uri = "~/foo.lance";
2417 write_to_file(&format!("{uri}/test_file"), "TILDE").unwrap();
2418 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
2419 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
2420 .await
2421 .unwrap();
2422 assert_eq!(contents, "TILDE");
2423 }
2424
2425 #[tokio::test]
2426 async fn test_read_directory() {
2427 let path = TempStdDir::default();
2428 create_dir_all(path.join("foo").join("bar")).unwrap();
2429 create_dir_all(path.join("foo").join("zoo")).unwrap();
2430 create_dir_all(path.join("foo").join("zoo").join("abc")).unwrap();
2431 write_to_file(
2432 path.join("foo").join("test_file").to_str().unwrap(),
2433 "read_dir",
2434 )
2435 .unwrap();
2436 let (store, base) = ObjectStore::from_uri(path.to_str().unwrap()).await.unwrap();
2437
2438 let sub_dirs = store.read_dir(base.clone().join("foo")).await.unwrap();
2439 assert_eq!(sub_dirs, vec!["bar", "zoo", "test_file"]);
2440 }
2441
2442 #[tokio::test]
2443 async fn test_delete_directory_local_store() {
2444 test_delete_directory("").await;
2445 }
2446
2447 #[tokio::test]
2448 async fn test_delete_directory_file_object_store() {
2449 test_delete_directory("file-object-store").await;
2450 }
2451
2452 async fn test_delete_directory(scheme: &str) {
2453 let path = TempStdDir::default();
2454 create_dir_all(path.join("foo").join("bar")).unwrap();
2455 create_dir_all(path.join("foo").join("zoo")).unwrap();
2456 create_dir_all(path.join("foo").join("zoo").join("abc")).unwrap();
2457 write_to_file(
2458 path.join("foo")
2459 .join("bar")
2460 .join("test_file")
2461 .to_str()
2462 .unwrap(),
2463 "delete",
2464 )
2465 .unwrap();
2466 let file_url = Url::from_directory_path(&path).unwrap();
2467 let url = if scheme.is_empty() {
2468 file_url
2469 } else {
2470 let mut url = Url::parse(&format!("{scheme}:///")).unwrap();
2471 url.set_path(file_url.path());
2473 url
2474 };
2475 let (store, base) = ObjectStore::from_uri(url.as_ref()).await.unwrap();
2476 store
2477 .remove_dir_all(base.clone().join("foo"))
2478 .await
2479 .unwrap();
2480
2481 assert!(!path.join("foo").exists());
2482 }
2483
2484 #[rstest]
2485 #[case("file")]
2486 #[case("file-object-store")]
2487 #[tokio::test]
2488 async fn test_remove_empty_directories(#[case] scheme: &str) {
2489 let path = TempStdDir::default();
2490 let stale_dir = path.join("stale");
2491 let nested_stale_dir = path.join("nested_stale");
2492 let nested_stale_child = nested_stale_dir.join("child");
2493 create_dir_all(&stale_dir).unwrap();
2494 create_dir_all(&nested_stale_child).unwrap();
2495 create_dir_all(path.join("retained").join("child")).unwrap();
2496 write_to_file(
2497 path.join("file_bearing")
2498 .join("test_file")
2499 .to_str()
2500 .unwrap(),
2501 "keep",
2502 )
2503 .unwrap();
2504 create_dir_all(path.join("file_bearing").join("empty_child")).unwrap();
2505
2506 let file_url = Url::from_directory_path(&path).unwrap();
2507 let mut url = Url::parse(&format!("{scheme}:///")).unwrap();
2508 url.set_path(file_url.path());
2509 let (store, base) = ObjectStore::from_uri(url.as_ref()).await.unwrap();
2510
2511 #[cfg(unix)]
2512 let unmodified_since = {
2513 let old_modified_time =
2514 std::time::SystemTime::now() - std::time::Duration::from_secs(10 * 24 * 60 * 60);
2515 for directory in [&stale_dir, &nested_stale_dir, &nested_stale_child] {
2516 std::fs::File::open(directory)
2517 .unwrap()
2518 .set_times(std::fs::FileTimes::new().set_modified(old_modified_time))
2519 .unwrap();
2520 }
2521 DateTime::<Utc>::from(std::time::SystemTime::now())
2522 - chrono::TimeDelta::try_days(7).unwrap()
2523 };
2524 #[cfg(not(unix))]
2525 let unmodified_since = DateTime::<Utc>::from(std::time::SystemTime::now())
2526 + chrono::TimeDelta::try_days(1).unwrap();
2527
2528 store
2529 .remove_empty_dirs(
2530 base.clone(),
2531 HashSet::from([base.clone().join("retained")]),
2532 HashSet::new(),
2533 Some(unmodified_since),
2534 )
2535 .await
2536 .unwrap();
2537
2538 assert!(!path.join("stale").exists());
2539 assert!(!path.join("nested_stale").exists());
2540 assert!(path.join("retained").join("child").exists());
2541 assert!(path.join("file_bearing").join("empty_child").exists());
2542
2543 create_dir_all(path.join("fresh")).unwrap();
2544 create_dir_all(path.join("verified")).unwrap();
2545 store
2546 .remove_empty_dirs(
2547 base.clone(),
2548 HashSet::from([base.clone().join("retained")]),
2549 HashSet::from([base.clone().join("verified")]),
2550 Some(
2551 DateTime::<Utc>::from(std::time::SystemTime::now())
2552 - chrono::TimeDelta::try_days(7).unwrap(),
2553 ),
2554 )
2555 .await
2556 .unwrap();
2557
2558 assert!(path.join("fresh").exists());
2559 assert!(!path.join("verified").exists());
2560 }
2561
2562 #[derive(Debug)]
2563 struct TestWrapper {
2564 called: AtomicBool,
2565
2566 return_value: Arc<dyn OSObjectStore>,
2567 }
2568
2569 impl WrappingObjectStore for TestWrapper {
2570 fn wrap(
2571 &self,
2572 _store_prefix: &str,
2573 _original: Arc<dyn OSObjectStore>,
2574 ) -> Arc<dyn OSObjectStore> {
2575 self.called.store(true, Ordering::Relaxed);
2576
2577 self.return_value.clone()
2579 }
2580
2581 fn wrap_paginated(
2584 &self,
2585 _store_prefix: &str,
2586 _original: Arc<dyn PaginatedListStore>,
2587 ) -> Option<Arc<dyn PaginatedListStore>> {
2588 None
2589 }
2590 }
2591
2592 impl TestWrapper {
2593 fn called(&self) -> bool {
2594 self.called.load(Ordering::Relaxed)
2595 }
2596 }
2597
2598 #[derive(Debug)]
2600 struct StubLister;
2601
2602 #[async_trait]
2603 impl PaginatedListStore for StubLister {
2604 async fn list_paginated(
2605 &self,
2606 _prefix: Option<&str>,
2607 _opts: object_store::list::PaginatedListOptions,
2608 ) -> object_store::Result<object_store::list::PaginatedListResult> {
2609 unimplemented!("this lister exists to be wrapped, not to list")
2610 }
2611 }
2612
2613 #[derive(Debug)]
2615 struct PaginatedTestWrapper {
2616 name: &'static str,
2617 log: Arc<std::sync::Mutex<Vec<String>>>,
2618 }
2619
2620 impl WrappingObjectStore for PaginatedTestWrapper {
2621 fn wrap(
2622 &self,
2623 _store_prefix: &str,
2624 original: Arc<dyn OSObjectStore>,
2625 ) -> Arc<dyn OSObjectStore> {
2626 original
2627 }
2628
2629 fn wrap_paginated(
2630 &self,
2631 store_prefix: &str,
2632 original: Arc<dyn PaginatedListStore>,
2633 ) -> Option<Arc<dyn PaginatedListStore>> {
2634 self.log
2635 .lock()
2636 .unwrap()
2637 .push(format!("{}@{store_prefix}", self.name));
2638 Some(original)
2639 }
2640 }
2641
2642 #[rstest]
2646 #[case::every_wrapper_keeps_it(false, vec!["first@memory", "second@memory"])]
2647 #[case::one_wrapper_gives_it_up(true, vec!["first@memory"])]
2648 fn test_a_chain_wraps_the_lister_until_one_gives_it_up(
2649 #[case] gives_up: bool,
2650 #[case] expected_log: Vec<&str>,
2651 ) {
2652 let log = Arc::new(std::sync::Mutex::new(Vec::new()));
2653 let mut wrappers: Vec<Arc<dyn WrappingObjectStore>> =
2654 vec![Arc::new(PaginatedTestWrapper {
2655 name: "first",
2656 log: log.clone(),
2657 })];
2658 if gives_up {
2659 wrappers.push(Arc::new(TestWrapper {
2660 called: AtomicBool::new(false),
2661 return_value: Arc::new(InMemory::new()),
2662 }));
2663 }
2664 wrappers.push(Arc::new(PaginatedTestWrapper {
2665 name: "second",
2666 log: log.clone(),
2667 }));
2668
2669 let wrapped = ChainedWrappingObjectStore::new(wrappers)
2670 .wrap_paginated("memory", Arc::new(StubLister));
2671
2672 assert_eq!(wrapped.is_none(), gives_up);
2673 assert_eq!(*log.lock().unwrap(), expected_log);
2674 }
2675
2676 #[rstest]
2680 #[case::gives_up_the_pushdown(true)]
2681 #[case::keeps_the_pushdown(false)]
2682 fn test_apply_wrapper_keeps_inner_and_the_lister_in_sync(#[case] gives_up: bool) {
2683 let replacement = Arc::new(InMemory::new());
2684 let giving_up = TestWrapper {
2685 called: AtomicBool::new(false),
2686 return_value: replacement.clone(),
2687 };
2688 let keeping = PaginatedTestWrapper {
2689 name: "passthrough",
2690 log: Arc::new(std::sync::Mutex::new(Vec::new())),
2691 };
2692 let wrapper: &dyn WrappingObjectStore = match gives_up {
2693 true => &giving_up,
2694 false => &keeping,
2695 };
2696
2697 let mut store = ObjectStore::memory();
2698 store.paginated_lister = Some(Arc::new(StubLister) as Arc<dyn PaginatedListStore>);
2699 store.apply_wrapper(wrapper);
2700
2701 assert_eq!(
2702 store.paginated_lister.is_some(),
2703 !gives_up,
2704 "the lister has to follow what the wrapper said"
2705 );
2706 assert_eq!(
2709 Arc::ptr_eq(&store.inner, &(replacement as Arc<dyn OSObjectStore>)),
2710 gives_up
2711 );
2712 }
2713
2714 #[tokio::test]
2715 async fn test_wrapper_identity_is_stable_across_tasks() {
2716 let wrapper = Arc::new(TestWrapper {
2717 called: AtomicBool::new(false),
2718 return_value: Arc::new(InMemory::new()),
2719 });
2720 let initial_params = ObjectStoreParams {
2721 object_store_wrapper: Some(wrapper.clone()),
2722 ..ObjectStoreParams::default()
2723 };
2724 let task_params = tokio::spawn(async move {
2725 ObjectStoreParams {
2726 object_store_wrapper: Some(wrapper),
2727 ..ObjectStoreParams::default()
2728 }
2729 })
2730 .await
2731 .unwrap();
2732
2733 assert_eq!(initial_params, task_params);
2734
2735 let mut initial_hasher = std::hash::DefaultHasher::new();
2736 std::hash::Hash::hash(&initial_params, &mut initial_hasher);
2737 let mut task_hasher = std::hash::DefaultHasher::new();
2738 std::hash::Hash::hash(&task_params, &mut task_hasher);
2739 assert_eq!(
2740 std::hash::Hasher::finish(&initial_hasher),
2741 std::hash::Hasher::finish(&task_hasher)
2742 );
2743 }
2744
2745 #[tokio::test]
2746 async fn test_wrapping_object_store_option_is_used() {
2747 let mock_inner_store: Arc<dyn OSObjectStore> = Arc::new(InMemory::new());
2749 let registry = Arc::new(ObjectStoreRegistry::default());
2750
2751 assert_eq!(Arc::strong_count(&mock_inner_store), 1);
2752
2753 let wrapper = Arc::new(TestWrapper {
2754 called: AtomicBool::new(false),
2755 return_value: mock_inner_store.clone(),
2756 });
2757
2758 let params = ObjectStoreParams {
2759 object_store_wrapper: Some(wrapper.clone()),
2760 ..ObjectStoreParams::default()
2761 };
2762
2763 assert!(!wrapper.called());
2765
2766 let _ = ObjectStore::from_uri_and_params(registry, "memory:///", ¶ms)
2767 .await
2768 .unwrap();
2769
2770 assert!(wrapper.called());
2772
2773 assert_eq!(Arc::strong_count(&mock_inner_store), 2);
2776 }
2777
2778 #[tokio::test]
2779 async fn test_local_paths() {
2780 let file_path = TempStdFile::default();
2781 let mut writer = ObjectStore::create_local_writer(&file_path).await.unwrap();
2782 writer.write_all(b"LOCAL").await.unwrap();
2783 Writer::shutdown(&mut writer).await.unwrap();
2784
2785 let reader = ObjectStore::open_local(&file_path).await.unwrap();
2786 let buf = reader.get_range(0..5).await.unwrap();
2787 assert_eq!(buf.as_ref(), b"LOCAL");
2788 }
2789
2790 #[cfg(unix)]
2791 #[tokio::test]
2792 async fn test_direct_local_writer_uses_standard_file_permissions() {
2793 let directory = TempStdDir::default();
2794 let reference_path = directory.join("reference");
2795 std::fs::File::create(&reference_path).unwrap();
2796 let expected_mode = std::fs::metadata(reference_path)
2797 .unwrap()
2798 .permissions()
2799 .mode()
2800 & 0o777;
2801
2802 let output_path = directory.join("output");
2803 let object_path = Path::from_absolute_path(&output_path).unwrap();
2804 let store = ObjectStore::local();
2805 let mut writer = store.create(&object_path).await.unwrap();
2806 writer.write_all(b"LOCAL").await.unwrap();
2807 Writer::shutdown(writer.as_mut()).await.unwrap();
2808
2809 let actual_mode = std::fs::metadata(output_path).unwrap().permissions().mode() & 0o777;
2810 assert_eq!(actual_mode, expected_mode);
2811 }
2812
2813 #[tokio::test]
2814 async fn test_read_one() {
2815 let file_path = TempStdFile::default();
2816 let mut writer = ObjectStore::create_local_writer(&file_path).await.unwrap();
2817 writer.write_all(b"LOCAL").await.unwrap();
2818 Writer::shutdown(&mut writer).await.unwrap();
2819
2820 let file_path_os = object_store::path::Path::parse(file_path.to_str().unwrap()).unwrap();
2821 let obj_store = ObjectStore::local();
2822 let buf = obj_store.read_one_all(&file_path_os).await.unwrap();
2823 assert_eq!(buf.as_ref(), b"LOCAL");
2824
2825 let buf = obj_store.read_one_range(&file_path_os, 0..5).await.unwrap();
2826 assert_eq!(buf.as_ref(), b"LOCAL");
2827 }
2828
2829 #[tokio::test]
2830 #[cfg(windows)]
2831 async fn test_windows_paths() {
2832 use std::path::Component;
2833 use std::path::Prefix;
2834 use std::path::Prefix::*;
2835
2836 fn get_path_prefix(path: &StdPath) -> Prefix<'_> {
2837 match path.components().next().unwrap() {
2838 Component::Prefix(prefix_component) => prefix_component.kind(),
2839 _ => panic!(),
2840 }
2841 }
2842
2843 fn get_drive_letter(prefix: Prefix) -> String {
2844 match prefix {
2845 Disk(bytes) => String::from_utf8(vec![bytes]).unwrap(),
2846 _ => panic!(),
2847 }
2848 }
2849
2850 let tmp_path = TempStdFile::default();
2851 let prefix = get_path_prefix(&tmp_path);
2852 let drive_letter = get_drive_letter(prefix);
2853
2854 write_to_file(
2855 &(format!("{drive_letter}:/test_folder/test.lance") + "/test_file"),
2856 "WINDOWS",
2857 )
2858 .unwrap();
2859
2860 for uri in &[
2861 format!("{drive_letter}:/test_folder/test.lance"),
2862 format!("{drive_letter}:\\test_folder\\test.lance"),
2863 ] {
2864 let (store, base) = ObjectStore::from_uri(uri).await.unwrap();
2865 let contents = read_from_store(store.as_ref(), &base.clone().join("test_file"))
2866 .await
2867 .unwrap();
2868 assert_eq!(contents, "WINDOWS");
2869 }
2870 }
2871
2872 #[tokio::test]
2873 async fn test_cross_filesystem_copy() {
2874 let source_dir = TempStdDir::default();
2876 let dest_dir = TempStdDir::default();
2877
2878 let source_file_name = "test_file.txt";
2880 let source_file = source_dir.join(source_file_name);
2881 std::fs::write(&source_file, b"test content").unwrap();
2882
2883 let (store, base_path) = ObjectStore::from_uri(source_dir.to_str().unwrap())
2885 .await
2886 .unwrap();
2887
2888 let from_path = base_path.clone().join(source_file_name);
2890
2891 let dest_file = dest_dir.join("copied_file.txt");
2893 let dest_str = dest_file.to_str().unwrap();
2894 let to_path = object_store::path::Path::parse(dest_str).unwrap();
2895
2896 store.copy(&from_path, &to_path).await.unwrap();
2898
2899 assert!(dest_file.exists());
2901 let copied_content = std::fs::read(&dest_file).unwrap();
2902 assert_eq!(copied_content, b"test content");
2903 }
2904
2905 #[tokio::test]
2906 async fn test_copy_creates_parent_directories() {
2907 let source_dir = TempStdDir::default();
2908 let dest_dir = TempStdDir::default();
2909
2910 let source_file_name = "test_file.txt";
2912 let source_file = source_dir.join(source_file_name);
2913 std::fs::write(&source_file, b"test content").unwrap();
2914
2915 let (store, base_path) = ObjectStore::from_uri(source_dir.to_str().unwrap())
2917 .await
2918 .unwrap();
2919
2920 let from_path = base_path.clone().join(source_file_name);
2922
2923 let dest_file = dest_dir.join("nested").join("dirs").join("copied_file.txt");
2925 let dest_str = dest_file.to_str().unwrap();
2926 let to_path = object_store::path::Path::parse(dest_str).unwrap();
2927
2928 store.copy(&from_path, &to_path).await.unwrap();
2930
2931 assert!(dest_file.exists());
2933 assert!(dest_file.parent().unwrap().exists());
2934 let copied_content = std::fs::read(&dest_file).unwrap();
2935 assert_eq!(copied_content, b"test content");
2936 }
2937
2938 #[derive(Debug)]
2943 struct CopyFailingStore {
2944 inner: InMemory,
2945 }
2946
2947 impl Display for CopyFailingStore {
2948 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2949 write!(f, "CopyFailingStore")
2950 }
2951 }
2952
2953 #[derive(Debug, Default)]
2954 struct MultipartObservations {
2955 part_count: AtomicUsize,
2956 abort_count: AtomicUsize,
2957 native_copy_count: AtomicUsize,
2958 delete_stream_count: AtomicUsize,
2959 delete_in_flight: AtomicUsize,
2960 delete_max_in_flight: AtomicUsize,
2962 }
2963
2964 struct InFlightGuard(Arc<MultipartObservations>);
2966
2967 impl Drop for InFlightGuard {
2968 fn drop(&mut self) {
2969 self.0.delete_in_flight.fetch_sub(1, Ordering::SeqCst);
2970 }
2971 }
2972
2973 #[derive(Debug)]
2974 struct ObservedMultipartUpload {
2975 inner: Box<dyn MultipartUpload>,
2976 observations: Arc<MultipartObservations>,
2977 fail_parts: bool,
2978 }
2979
2980 #[async_trait]
2981 impl MultipartUpload for ObservedMultipartUpload {
2982 fn put_part(&mut self, data: PutPayload) -> UploadPart {
2983 self.observations.part_count.fetch_add(1, Ordering::SeqCst);
2984 if self.fail_parts {
2985 return Box::pin(async {
2986 Err(object_store::Error::Generic {
2987 store: "ObservedMultipartStore",
2988 source: "injected multipart part failure".into(),
2989 })
2990 });
2991 }
2992 self.inner.put_part(data)
2993 }
2994
2995 async fn complete(&mut self) -> OSResult<PutResult> {
2996 self.inner.complete().await
2997 }
2998
2999 async fn abort(&mut self) -> OSResult<()> {
3000 self.observations.abort_count.fetch_add(1, Ordering::SeqCst);
3001 self.inner.abort().await
3002 }
3003 }
3004
3005 #[derive(Debug)]
3006 struct ObservedMultipartStore {
3007 inner: InMemory,
3008 observations: Arc<MultipartObservations>,
3009 fail_parts: bool,
3010 destination_size_adjustment: u64,
3011 }
3012
3013 impl Display for ObservedMultipartStore {
3014 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
3015 write!(f, "ObservedMultipartStore")
3016 }
3017 }
3018
3019 #[async_trait]
3020 impl OSObjectStore for ObservedMultipartStore {
3021 async fn put_opts(
3022 &self,
3023 location: &Path,
3024 bytes: PutPayload,
3025 opts: PutOptions,
3026 ) -> OSResult<PutResult> {
3027 self.inner.put_opts(location, bytes, opts).await
3028 }
3029
3030 async fn put_multipart_opts(
3031 &self,
3032 location: &Path,
3033 opts: PutMultipartOptions,
3034 ) -> OSResult<Box<dyn MultipartUpload>> {
3035 let inner = self.inner.put_multipart_opts(location, opts).await?;
3036 Ok(Box::new(ObservedMultipartUpload {
3037 inner,
3038 observations: self.observations.clone(),
3039 fail_parts: self.fail_parts,
3040 }))
3041 }
3042
3043 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
3044 let is_head = options.head;
3045 let mut result = self.inner.get_opts(location, options).await?;
3046 if is_head && location.filename() == Some("destination.bin") {
3047 result.meta.size = result
3048 .meta
3049 .size
3050 .checked_add(self.destination_size_adjustment)
3051 .expect("test destination size should not overflow");
3052 }
3053 Ok(result)
3054 }
3055
3056 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
3057 self.inner.get_ranges(location, ranges).await
3058 }
3059
3060 fn delete_stream(
3061 &self,
3062 locations: BoxStream<'static, OSResult<Path>>,
3063 ) -> BoxStream<'static, OSResult<Path>> {
3064 let observations = self.observations.clone();
3065 observations
3066 .delete_stream_count
3067 .fetch_add(1, Ordering::SeqCst);
3068 let in_flight = observations.delete_in_flight.fetch_add(1, Ordering::SeqCst) + 1;
3069 observations
3070 .delete_max_in_flight
3071 .fetch_max(in_flight, Ordering::SeqCst);
3072 let guard = InFlightGuard(observations);
3073 let inner = self.inner.delete_stream(locations);
3074 async move {
3077 tokio::task::yield_now().await;
3078 tokio::time::sleep(Duration::from_millis(5)).await;
3079 inner.map(move |result| {
3080 let _ = &guard;
3081 result
3082 })
3083 }
3084 .flatten_stream()
3085 .boxed()
3086 }
3087
3088 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
3089 self.inner.list(prefix)
3090 }
3091
3092 fn list_with_offset(
3093 &self,
3094 prefix: Option<&Path>,
3095 offset: &Path,
3096 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
3097 self.inner.list_with_offset(prefix, offset)
3098 }
3099
3100 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
3101 self.inner.list_with_delimiter(prefix).await
3102 }
3103
3104 async fn copy_opts(&self, from: &Path, to: &Path, opts: CopyOptions) -> OSResult<()> {
3105 self.observations
3106 .native_copy_count
3107 .fetch_add(1, Ordering::SeqCst);
3108 self.inner.copy_opts(from, to, opts).await
3109 }
3110 }
3111
3112 #[async_trait]
3113 impl OSObjectStore for CopyFailingStore {
3114 async fn put_opts(
3115 &self,
3116 location: &Path,
3117 bytes: PutPayload,
3118 opts: PutOptions,
3119 ) -> OSResult<PutResult> {
3120 self.inner.put_opts(location, bytes, opts).await
3121 }
3122 async fn put_multipart_opts(
3123 &self,
3124 location: &Path,
3125 opts: PutMultipartOptions,
3126 ) -> OSResult<Box<dyn MultipartUpload>> {
3127 self.inner.put_multipart_opts(location, opts).await
3128 }
3129 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
3130 self.inner.get_opts(location, options).await
3131 }
3132 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
3133 self.inner.get_ranges(location, ranges).await
3134 }
3135 fn delete_stream(
3136 &self,
3137 locations: BoxStream<'static, OSResult<Path>>,
3138 ) -> BoxStream<'static, OSResult<Path>> {
3139 self.inner.delete_stream(locations)
3140 }
3141 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
3142 self.inner.list(prefix)
3143 }
3144 fn list_with_offset(
3145 &self,
3146 prefix: Option<&Path>,
3147 offset: &Path,
3148 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
3149 self.inner.list_with_offset(prefix, offset)
3150 }
3151 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
3152 self.inner.list_with_delimiter(prefix).await
3153 }
3154 async fn copy_opts(&self, _from: &Path, _to: &Path, _opts: CopyOptions) -> OSResult<()> {
3155 Err(object_store::Error::Generic {
3156 store: "CopyFailingStore",
3157 source: "single-shot copy disabled in test".into(),
3158 })
3159 }
3160 }
3161
3162 #[tokio::test]
3163 async fn test_copy_streams_objects_larger_than_threshold() {
3164 let mut store = ObjectStore::memory();
3170 store.inner = Arc::new(CopyFailingStore {
3171 inner: InMemory::new(),
3172 });
3173
3174 let from = Path::from("source.bin");
3175 let contents = b"streaming multipart copy payload well past the tiny threshold";
3176 store.put(&from, contents).await.unwrap();
3177
3178 let streamed = Path::from("streamed.bin");
3181 store.copy_impl(&from, &streamed, true, 8).await.unwrap();
3182 let copied = store.read_one_all(&streamed).await.unwrap();
3183 assert_eq!(copied.as_ref(), contents.as_slice());
3184
3185 let native = Path::from("native.bin");
3189 assert!(
3190 store
3191 .copy_impl(&from, &native, true, u64::MAX)
3192 .await
3193 .is_err()
3194 );
3195 }
3196
3197 #[tokio::test]
3198 async fn test_copy_via_stream_never_uses_native_copy() {
3199 let mut store = ObjectStore::memory();
3200 store.inner = Arc::new(CopyFailingStore {
3201 inner: InMemory::new(),
3202 });
3203
3204 let source = Path::from("source.bin");
3205 let destination = Path::from("destination.bin");
3206 let contents = b"stream raw bytes instead of issuing native copy";
3207 store.put(&source, contents).await.unwrap();
3208
3209 let result = store
3210 .copy_via_stream(&source, &store, &destination)
3211 .await
3212 .unwrap();
3213
3214 assert_eq!(result.size, contents.len());
3215 assert_eq!(
3216 store.read_one_all(&destination).await.unwrap().as_ref(),
3217 contents
3218 );
3219 }
3220
3221 #[tokio::test]
3222 async fn test_copy_if_not_exists_accepts_equal_size_local_retry() {
3223 let directory = TempStdDir::default();
3224 let (store, base_path) = ObjectStore::from_uri(directory.to_str().unwrap())
3225 .await
3226 .unwrap();
3227 let source = base_path.clone().join("source.bin");
3228 let destination = base_path.join("destination.bin");
3229 store.put(&source, b"new").await.unwrap();
3230 store.put(&destination, b"old").await.unwrap();
3231
3232 let result = store
3233 .copy_if_not_exists(&source, &destination)
3234 .await
3235 .unwrap();
3236
3237 assert_eq!(result.size, 3);
3238 assert_eq!(
3239 store.read_one_all(&destination).await.unwrap().as_ref(),
3240 b"old"
3241 );
3242 }
3243
3244 #[tokio::test]
3245 async fn test_copy_if_not_exists_uses_native_copy() {
3246 let observations = Arc::new(MultipartObservations::default());
3247 let mut store = ObjectStore::memory();
3248 store.inner = Arc::new(ObservedMultipartStore {
3249 inner: InMemory::new(),
3250 observations: observations.clone(),
3251 fail_parts: false,
3252 destination_size_adjustment: 0,
3253 });
3254
3255 let source = Path::from("source.bin");
3256 let destination = Path::from("destination.bin");
3257 let contents = vec![42; crate::object_writer::initial_upload_size() + 1];
3258 store.put(&source, &contents).await.unwrap();
3259 observations.part_count.store(0, Ordering::SeqCst);
3260
3261 let result = store
3262 .copy_if_not_exists(&source, &destination)
3263 .await
3264 .unwrap();
3265
3266 assert_eq!(result.size, contents.len());
3267 assert_eq!(observations.native_copy_count.load(Ordering::SeqCst), 1);
3268 assert_eq!(observations.part_count.load(Ordering::SeqCst), 0);
3269 assert_eq!(
3270 store.read_one_all(&destination).await.unwrap().as_ref(),
3271 contents.as_slice()
3272 );
3273 }
3274
3275 #[tokio::test]
3276 async fn test_remove_stream_removes_every_path() {
3277 let store = ObjectStore::memory();
3278 let paths: Vec<Path> = (0..64).map(|i| Path::from(format!("obj-{i:03}"))).collect();
3279 for path in &paths {
3280 store.put(path, b"x").await.unwrap();
3281 }
3282
3283 let to_remove = futures::stream::iter(paths.clone().into_iter().map(Ok)).boxed();
3284 let mut reported = store
3285 .remove_stream(to_remove)
3286 .try_collect::<Vec<_>>()
3287 .await
3288 .unwrap();
3289
3290 reported.sort_unstable();
3292 let mut expected = paths.clone();
3293 expected.sort_unstable();
3294 assert_eq!(reported, expected);
3295 for path in &paths {
3296 assert!(!store.exists(path).await.unwrap(), "{path} still present");
3297 }
3298 }
3299
3300 #[tokio::test]
3301 async fn test_remove_stream_tolerates_already_deleted() {
3302 let store = ObjectStore::memory();
3305 let present = Path::from("present");
3306 let absent = Path::from("never-written");
3307 store.put(&present, b"x").await.unwrap();
3308
3309 let to_remove =
3310 futures::stream::iter(vec![Ok(absent.clone()), Ok(present.clone())]).boxed();
3311 let mut removed = store
3312 .remove_stream(to_remove)
3313 .try_collect::<Vec<_>>()
3314 .await
3315 .expect("an already-absent path must not fail the stream");
3316
3317 removed.sort_unstable();
3318 let mut expected = vec![absent, present.clone()];
3319 expected.sort_unstable();
3320 assert_eq!(removed, expected);
3321 assert!(!store.exists(&present).await.unwrap());
3322 }
3323
3324 #[tokio::test]
3325 async fn test_remove_stream_deletes_concurrently() {
3326 let observations = Arc::new(MultipartObservations::default());
3329 let mut store = ObjectStore::memory();
3330 store.inner = Arc::new(ObservedMultipartStore {
3331 inner: InMemory::new(),
3332 observations: observations.clone(),
3333 fail_parts: false,
3334 destination_size_adjustment: 0,
3335 });
3336
3337 let paths: Vec<Path> = (0..64).map(|i| Path::from(format!("obj-{i:03}"))).collect();
3338 for path in &paths {
3339 store.put(path, b"x").await.unwrap();
3340 }
3341
3342 let to_remove = futures::stream::iter(paths.clone().into_iter().map(Ok)).boxed();
3343 let removed = store
3344 .remove_stream(to_remove)
3345 .try_collect::<Vec<_>>()
3346 .await
3347 .unwrap();
3348
3349 assert_eq!(removed.len(), paths.len());
3350 assert!(
3351 observations.delete_max_in_flight.load(Ordering::SeqCst) > 1,
3352 "deletions ran one at a time; remove_stream must overlap them"
3353 );
3354 }
3355
3356 #[tokio::test]
3357 async fn test_bulk_copy_streams_when_server_side_copy_is_disabled() {
3358 let observations = Arc::new(MultipartObservations::default());
3359 let mut store = ObjectStore::memory();
3360 store.inner = Arc::new(ObservedMultipartStore {
3361 inner: InMemory::new(),
3362 observations: observations.clone(),
3363 fail_parts: false,
3364 destination_size_adjustment: 0,
3365 });
3366
3367 let source = Path::from("source.bin");
3368 let destination = Path::from("destination.bin");
3369 let contents = b"stream by default";
3370 store.put(&source, contents).await.unwrap();
3371
3372 let result = store
3373 .copy_bulk_with_server_side_copy(&source, &store, &destination, false)
3374 .await
3375 .unwrap();
3376
3377 assert_eq!(result.size, contents.len());
3378 assert_eq!(observations.native_copy_count.load(Ordering::SeqCst), 0);
3379 assert_eq!(
3380 store.read_one_all(&destination).await.unwrap().as_ref(),
3381 contents
3382 );
3383 }
3384
3385 #[test]
3386 #[serial(server_side_copy_env)]
3387 fn test_server_side_copy_environment_policy() {
3388 let previous_value = std::env::var_os(SERVER_SIDE_COPY_ENABLED_ENV);
3389 let mut store = ObjectStore::memory();
3390 store.scheme = "test-cloud".to_string();
3391 let destination_store = store.clone();
3392
3393 unsafe { std::env::remove_var(SERVER_SIDE_COPY_ENABLED_ENV) };
3396 assert!(!store.uses_server_side_copy(&destination_store));
3397
3398 unsafe { std::env::set_var(SERVER_SIDE_COPY_ENABLED_ENV, "true") };
3400 assert!(store.uses_server_side_copy(&destination_store));
3401
3402 unsafe {
3404 match previous_value {
3405 Some(value) => std::env::set_var(SERVER_SIDE_COPY_ENABLED_ENV, value),
3406 None => std::env::remove_var(SERVER_SIDE_COPY_ENABLED_ENV),
3407 }
3408 }
3409 }
3410
3411 #[tokio::test]
3412 async fn test_bulk_copy_uses_server_side_copy_when_enabled_for_same_store() {
3413 let observations = Arc::new(MultipartObservations::default());
3414 let mut source_store = ObjectStore::memory();
3415 source_store.scheme = "test-cloud".to_string();
3416 source_store.inner = Arc::new(ObservedMultipartStore {
3417 inner: InMemory::new(),
3418 observations: observations.clone(),
3419 fail_parts: false,
3420 destination_size_adjustment: 0,
3421 });
3422 let destination_store = source_store.clone();
3423
3424 let source = Path::from("source.bin");
3425 let destination = Path::from("destination.bin");
3426 let contents = b"use native copy when explicitly enabled";
3427 source_store.put(&source, contents).await.unwrap();
3428
3429 let result = source_store
3430 .copy_bulk_with_server_side_copy(&source, &destination_store, &destination, true)
3431 .await
3432 .unwrap();
3433
3434 assert_eq!(result.size, contents.len());
3435 assert_eq!(observations.native_copy_count.load(Ordering::SeqCst), 1);
3436 assert_eq!(
3437 destination_store
3438 .read_one_all(&destination)
3439 .await
3440 .unwrap()
3441 .as_ref(),
3442 contents
3443 );
3444 }
3445
3446 #[tokio::test]
3447 async fn test_bulk_copy_streams_for_distinct_clients_with_same_prefix() {
3448 let shared_inner = InMemory::new();
3449 let source_observations = Arc::new(MultipartObservations::default());
3450 let mut source_store = ObjectStore::memory();
3451 source_store.scheme = "test-cloud".to_string();
3452 source_store.store_prefix = "test-cloud$bucket".to_string();
3453 source_store.inner = Arc::new(ObservedMultipartStore {
3454 inner: shared_inner.clone(),
3455 observations: source_observations.clone(),
3456 fail_parts: false,
3457 destination_size_adjustment: 0,
3458 });
3459 let destination_observations = Arc::new(MultipartObservations::default());
3460 let mut destination_store = ObjectStore::memory();
3461 destination_store.scheme = "test-cloud".to_string();
3462 destination_store.store_prefix = "test-cloud$bucket".to_string();
3463 destination_store.inner = Arc::new(ObservedMultipartStore {
3464 inner: shared_inner,
3465 observations: destination_observations.clone(),
3466 fail_parts: false,
3467 destination_size_adjustment: 0,
3468 });
3469
3470 let source = Path::from("source.bin");
3471 let destination = Path::from("destination.bin");
3472 let contents = b"use native copy when explicitly enabled";
3473 source_store.put(&source, contents).await.unwrap();
3474
3475 let result = source_store
3476 .copy_bulk_with_server_side_copy(&source, &destination_store, &destination, true)
3477 .await
3478 .unwrap();
3479
3480 assert_eq!(result.size, contents.len());
3481 assert_eq!(
3482 source_observations.native_copy_count.load(Ordering::SeqCst),
3483 0
3484 );
3485 assert_eq!(
3486 destination_observations
3487 .native_copy_count
3488 .load(Ordering::SeqCst),
3489 0
3490 );
3491 assert_eq!(
3492 destination_store
3493 .read_one_all(&destination)
3494 .await
3495 .unwrap()
3496 .as_ref(),
3497 contents
3498 );
3499 }
3500
3501 #[tokio::test]
3502 async fn test_bulk_copy_rejects_server_side_destination_size_mismatch() {
3503 let observations = Arc::new(MultipartObservations::default());
3504 let mut source_store = ObjectStore::memory();
3505 source_store.scheme = "test-cloud".to_string();
3506 source_store.inner = Arc::new(ObservedMultipartStore {
3507 inner: InMemory::new(),
3508 observations: observations.clone(),
3509 fail_parts: false,
3510 destination_size_adjustment: 1,
3511 });
3512 let destination_store = source_store.clone();
3513
3514 let source = Path::from("source.bin");
3515 let destination = Path::from("destination.bin");
3516 source_store
3517 .put(&source, b"validate native copy")
3518 .await
3519 .unwrap();
3520
3521 let error = source_store
3522 .copy_bulk_with_server_side_copy(&source, &destination_store, &destination, true)
3523 .await
3524 .unwrap_err();
3525
3526 assert!(
3527 error.to_string().contains("destination size mismatch"),
3528 "expected validation failure, got: {error}"
3529 );
3530 assert_eq!(observations.native_copy_count.load(Ordering::SeqCst), 1);
3531 }
3532
3533 #[tokio::test]
3534 async fn test_bulk_copy_streams_across_stores_when_server_side_copy_is_enabled() {
3535 let source_store = ObjectStore::memory();
3536 let observations = Arc::new(MultipartObservations::default());
3537 let mut destination_store = ObjectStore::memory();
3538 destination_store.inner = Arc::new(ObservedMultipartStore {
3539 inner: InMemory::new(),
3540 observations: observations.clone(),
3541 fail_parts: false,
3542 destination_size_adjustment: 0,
3543 });
3544
3545 let source = Path::from("source.bin");
3546 let destination = Path::from("destination.bin");
3547 let contents = b"cross-store copies must stream";
3548 source_store.put(&source, contents).await.unwrap();
3549
3550 let result = source_store
3551 .copy_bulk_with_server_side_copy(&source, &destination_store, &destination, true)
3552 .await
3553 .unwrap();
3554
3555 assert_eq!(result.size, contents.len());
3556 assert_eq!(observations.native_copy_count.load(Ordering::SeqCst), 0);
3557 assert_eq!(
3558 destination_store
3559 .read_one_all(&destination)
3560 .await
3561 .unwrap()
3562 .as_ref(),
3563 contents
3564 );
3565 }
3566
3567 #[tokio::test]
3568 async fn test_copy_via_stream_preserves_local_not_found() {
3569 let directory = TempStdDir::default();
3570 let (store, base_path) = ObjectStore::from_uri(directory.to_str().unwrap())
3571 .await
3572 .unwrap();
3573 let source = base_path.clone().join("missing.bin");
3574 let destination = base_path.join("destination.bin");
3575
3576 let error = store
3577 .copy_via_stream(&source, &store, &destination)
3578 .await
3579 .unwrap_err();
3580
3581 assert!(
3582 error.is_not_found(),
3583 "expected not-found error, got: {error}"
3584 );
3585 }
3586
3587 #[tokio::test]
3588 async fn test_copy_via_stream_uses_multiple_parts() {
3589 let mut source_store = ObjectStore::memory();
3590 source_store.max_iop_size = 1024 * 1024;
3591 let observations = Arc::new(MultipartObservations::default());
3592 let mut destination_store = ObjectStore::memory();
3593 destination_store.inner = Arc::new(ObservedMultipartStore {
3594 inner: InMemory::new(),
3595 observations: observations.clone(),
3596 fail_parts: false,
3597 destination_size_adjustment: 0,
3598 });
3599
3600 let source = Path::from("source.bin");
3601 let destination = Path::from("destination.bin");
3602 let contents = vec![42; crate::object_writer::initial_upload_size() * 2 + 1];
3603 source_store.put(&source, &contents).await.unwrap();
3604
3605 let result = source_store
3606 .copy_via_stream(&source, &destination_store, &destination)
3607 .await
3608 .unwrap();
3609
3610 assert_eq!(result.size, contents.len());
3611 assert!(
3612 observations.part_count.load(Ordering::SeqCst) >= 2,
3613 "stream copy should split a large destination into multiple upload parts"
3614 );
3615 assert_eq!(
3616 destination_store
3617 .read_one_all(&destination)
3618 .await
3619 .unwrap()
3620 .as_ref(),
3621 contents.as_slice()
3622 );
3623 }
3624
3625 #[tokio::test]
3626 async fn test_copy_via_stream_aborts_failed_upload_and_retains_source() {
3627 let source_store = ObjectStore::memory();
3628 let observations = Arc::new(MultipartObservations::default());
3629 let mut destination_store = ObjectStore::memory();
3630 destination_store.inner = Arc::new(ObservedMultipartStore {
3631 inner: InMemory::new(),
3632 observations: observations.clone(),
3633 fail_parts: true,
3634 destination_size_adjustment: 0,
3635 });
3636
3637 let source = Path::from("source.bin");
3638 let destination = Path::from("destination.bin");
3639 let contents = vec![7; crate::object_writer::initial_upload_size() * 2];
3640 source_store.put(&source, &contents).await.unwrap();
3641
3642 let error = source_store
3643 .copy_via_stream(&source, &destination_store, &destination)
3644 .await
3645 .unwrap_err();
3646 let error_message = error.to_string();
3647 assert!(
3648 (error_message.contains("destination write")
3649 || error_message.contains("destination completion"))
3650 && error_message.contains("injected multipart part failure"),
3651 "expected upload-stage context and the underlying error, got: {error}"
3652 );
3653
3654 tokio::time::timeout(Duration::from_secs(1), async {
3655 loop {
3656 if observations.abort_count.load(Ordering::SeqCst) > 0 {
3657 break;
3658 }
3659 tokio::task::yield_now().await;
3660 }
3661 })
3662 .await
3663 .expect("multipart abort should complete");
3664 assert_eq!(observations.abort_count.load(Ordering::SeqCst), 1);
3665 assert_eq!(
3666 source_store.read_one_all(&source).await.unwrap().as_ref(),
3667 contents.as_slice()
3668 );
3669 assert!(!destination_store.exists(&destination).await.unwrap());
3670 }
3671
3672 #[tokio::test]
3673 async fn test_copy_via_stream_rejects_destination_size_mismatch() {
3674 let source_store = ObjectStore::memory();
3675 let mut destination_store = ObjectStore::memory();
3676 destination_store.inner = Arc::new(ObservedMultipartStore {
3677 inner: InMemory::new(),
3678 observations: Arc::new(MultipartObservations::default()),
3679 fail_parts: false,
3680 destination_size_adjustment: 1,
3681 });
3682
3683 let source = Path::from("source.bin");
3684 let destination = Path::from("destination.bin");
3685 let contents = b"validate the destination after completion";
3686 source_store.put(&source, contents).await.unwrap();
3687
3688 let error = source_store
3689 .copy_via_stream(&source, &destination_store, &destination)
3690 .await
3691 .unwrap_err();
3692
3693 assert!(
3694 error.to_string().contains("destination size mismatch"),
3695 "expected validation failure, got: {error}"
3696 );
3697 }
3698
3699 #[test]
3700 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
3701 fn test_client_options_extracts_headers() {
3702 let opts = StorageOptions(HashMap::from([
3703 ("headers.x-custom-foo".to_string(), "bar".to_string()),
3704 ("headers.x-ms-version".to_string(), "2023-11-03".to_string()),
3705 ("region".to_string(), "us-west-2".to_string()),
3706 ]));
3707 let client_options = opts.client_options().unwrap();
3708
3709 let opts_no_headers = StorageOptions(HashMap::from([(
3712 "region".to_string(),
3713 "us-west-2".to_string(),
3714 )]));
3715 opts_no_headers.client_options().unwrap();
3716
3717 #[cfg(feature = "gcp")]
3721 {
3722 use object_store::gcp::GoogleCloudStorageBuilder;
3723 let _builder = GoogleCloudStorageBuilder::new()
3724 .with_client_options(client_options)
3725 .with_url("gs://test-bucket");
3726 }
3727 }
3728
3729 #[test]
3730 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
3731 fn test_client_options_rejects_invalid_header_name() {
3732 let opts = StorageOptions(HashMap::from([(
3733 "headers.bad header".to_string(),
3734 "value".to_string(),
3735 )]));
3736 let err = opts.client_options().unwrap_err();
3737 assert!(err.to_string().contains("invalid header name"));
3738 }
3739
3740 #[test]
3741 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
3742 fn test_client_options_rejects_invalid_header_value() {
3743 let opts = StorageOptions(HashMap::from([(
3744 "headers.x-good-name".to_string(),
3745 "bad\x01value".to_string(),
3746 )]));
3747 let err = opts.client_options().unwrap_err();
3748 assert!(err.to_string().contains("invalid header value"));
3749 }
3750
3751 #[test]
3752 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
3753 fn test_client_options_empty_when_no_header_keys() {
3754 let opts = StorageOptions(HashMap::from([
3755 ("region".to_string(), "us-east-1".to_string()),
3756 ("access_key_id".to_string(), "AKID".to_string()),
3757 ]));
3758 opts.client_options().unwrap();
3759 }
3760}