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 scoped_to_base(&self, base_id: Option<u32>) -> Cow<'_, Self> {
406 let Some(accessor) = &self.storage_options_accessor else {
407 return Cow::Borrowed(self);
408 };
409 let scoped = accessor.scoped_to_base(base_id);
410 if Arc::ptr_eq(&scoped, accessor) {
411 Cow::Borrowed(self)
412 } else {
413 Cow::Owned(Self {
414 storage_options_accessor: Some(scoped),
415 ..self.clone()
416 })
417 }
418 }
419}
420
421fn wrapper_allocation_ptr(wrapper: &Arc<dyn WrappingObjectStore>) -> *const () {
422 Arc::as_ptr(wrapper) as *const ()
425}
426
427impl std::hash::Hash for ObjectStoreParams {
429 #[allow(deprecated)]
430 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
431 self.block_size.hash(state);
433 if let Some((store, url)) = &self.object_store {
434 Arc::as_ptr(store).hash(state);
435 url.hash(state);
436 }
437 self.s3_credentials_refresh_offset.hash(state);
438 #[cfg(feature = "aws")]
439 if let Some(aws_credentials) = &self.aws_credentials {
440 Arc::as_ptr(aws_credentials).hash(state);
441 }
442 if let Some(wrapper) = &self.object_store_wrapper {
443 wrapper_allocation_ptr(wrapper).hash(state);
444 }
445 if let Some(accessor) = &self.storage_options_accessor {
446 accessor.accessor_id().hash(state);
447 }
448 self.use_constant_size_upload_parts.hash(state);
449 self.list_is_lexically_ordered.hash(state);
450 }
451}
452
453impl Eq for ObjectStoreParams {}
455impl PartialEq for ObjectStoreParams {
456 #[allow(deprecated)]
457 fn eq(&self, other: &Self) -> bool {
458 #[cfg(feature = "aws")]
459 if self.aws_credentials.is_some() != other.aws_credentials.is_some() {
460 return false;
461 }
462
463 self.block_size == other.block_size
466 && self
467 .object_store
468 .as_ref()
469 .map(|(store, url)| (Arc::as_ptr(store), url))
470 == other
471 .object_store
472 .as_ref()
473 .map(|(store, url)| (Arc::as_ptr(store), url))
474 && self.s3_credentials_refresh_offset == other.s3_credentials_refresh_offset
475 && self
476 .object_store_wrapper
477 .as_ref()
478 .map(wrapper_allocation_ptr)
479 == other
480 .object_store_wrapper
481 .as_ref()
482 .map(wrapper_allocation_ptr)
483 && self
484 .storage_options_accessor
485 .as_ref()
486 .map(|a| a.accessor_id())
487 == other
488 .storage_options_accessor
489 .as_ref()
490 .map(|a| a.accessor_id())
491 && self.use_constant_size_upload_parts == other.use_constant_size_upload_parts
492 && self.list_is_lexically_ordered == other.list_is_lexically_ordered
493 }
494}
495
496pub fn uri_to_url(uri: &str) -> Result<Url> {
517 match Url::parse(uri) {
518 Ok(url) if url.scheme().len() == 1 && cfg!(windows) => {
519 local_path_to_url(uri)
521 }
522 Ok(url) => Ok(url),
523 Err(_) => local_path_to_url(uri),
524 }
525}
526
527fn expand_path(str_path: impl AsRef<str>) -> Result<std::path::PathBuf> {
528 let str_path = str_path.as_ref();
529 let expanded = expand_tilde_path(str_path).unwrap_or_else(|| str_path.into());
530
531 let mut expanded_path = path_abs::PathAbs::new(expanded)
532 .unwrap()
533 .as_path()
534 .to_path_buf();
535 if let Some(s) = expanded_path.as_path().to_str()
537 && s.is_empty()
538 {
539 expanded_path = std::env::current_dir()?;
540 }
541
542 Ok(expanded_path)
543}
544
545fn expand_tilde_path(path: &str) -> Option<std::path::PathBuf> {
546 let home_dir = std::env::home_dir()?;
547 if path == "~" {
548 return Some(home_dir);
549 }
550 if let Some(stripped) = path.strip_prefix("~/") {
551 return Some(home_dir.join(stripped));
552 }
553 #[cfg(windows)]
554 if let Some(stripped) = path.strip_prefix("~\\") {
555 return Some(home_dir.join(stripped));
556 }
557
558 None
559}
560
561fn local_path_to_url(str_path: &str) -> Result<Url> {
562 let expanded_path = expand_path(str_path)?;
563
564 Url::from_directory_path(expanded_path).map_err(|_| {
565 Error::invalid_input_source(format!("Invalid table location: '{}'", str_path).into())
566 })
567}
568
569#[cfg(feature = "huggingface")]
570fn parse_hf_repo_id(url: &Url) -> Result<String> {
571 let mut segments: Vec<String> = Vec::new();
573 if let Some(host) = url.host_str() {
574 segments.push(host.to_string());
575 }
576 segments.extend(
577 url.path()
578 .trim_start_matches('/')
579 .split('/')
580 .map(|s| s.to_string()),
581 );
582
583 if segments.len() < 2 {
584 return Err(Error::invalid_input(
585 "Huggingface URL must contain at least owner and repo",
586 ));
587 }
588
589 let repo_type_candidates = ["models", "datasets", "spaces"];
590 let (owner, repo_with_rev) = if repo_type_candidates.contains(&segments[0].as_str()) {
591 if segments.len() < 3 {
592 return Err(Error::invalid_input(
593 "Huggingface URL missing owner/repo after repo type",
594 ));
595 }
596 (segments[1].as_str(), segments[2].as_str())
597 } else {
598 (segments[0].as_str(), segments[1].as_str())
599 };
600
601 let repo = repo_with_rev
602 .split_once('@')
603 .map(|(r, _)| r)
604 .unwrap_or(repo_with_rev);
605 Ok(format!("{owner}/{repo}"))
606}
607
608impl ObjectStore {
609 pub async fn from_uri(uri: &str) -> Result<(Arc<Self>, Path)> {
617 let registry = Arc::new(ObjectStoreRegistry::default());
618
619 Self::from_uri_and_params(registry, uri, &ObjectStoreParams::default()).await
620 }
621
622 pub async fn from_uri_and_params(
626 registry: Arc<ObjectStoreRegistry>,
627 uri: &str,
628 params: &ObjectStoreParams,
629 ) -> Result<(Arc<Self>, Path)> {
630 Self::from_uri_and_params_impl(registry, uri, params, true).await
631 }
632
633 #[doc(hidden)]
638 pub async fn from_uri_and_params_uncached(
639 registry: Arc<ObjectStoreRegistry>,
640 uri: &str,
641 params: &ObjectStoreParams,
642 ) -> Result<(Arc<Self>, Path)> {
643 Self::from_uri_and_params_impl(registry, uri, params, false).await
644 }
645
646 async fn from_uri_and_params_impl(
647 registry: Arc<ObjectStoreRegistry>,
648 uri: &str,
649 params: &ObjectStoreParams,
650 use_registry_cache: bool,
651 ) -> Result<(Arc<Self>, Path)> {
652 #[allow(deprecated)]
653 if let Some((store, path)) = params.object_store.as_ref() {
654 let mut inner = store.clone();
655 let store_prefix =
656 registry.calculate_object_store_prefix(uri, params.storage_options())?;
657
658 let mut io_tracker = IOTracker::default();
659 meter_store(&mut inner, &mut io_tracker, &store_prefix);
660
661 if let Some(wrapper) = params.object_store_wrapper.as_ref() {
662 inner = wrapper.wrap(&store_prefix, inner);
663 }
664
665 let tracked_store = io_tracker.wrap("", inner);
667
668 let store = Self {
669 inner: tracked_store,
670 local_dir_operations: None,
671 scheme: path.scheme().to_string(),
672 block_size: params.block_size.unwrap_or(64 * 1024),
673 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
674 use_constant_size_upload_parts: params.use_constant_size_upload_parts,
675 list_is_lexically_ordered: params.list_is_lexically_ordered.unwrap_or_default(),
676 io_parallelism: DEFAULT_CLOUD_IO_PARALLELISM,
677 download_retry_count: DEFAULT_DOWNLOAD_RETRY_COUNT,
678 io_tracker,
679 store_prefix,
680 paginated_lister: None,
682 };
683 let path = Path::parse(path.path())?;
684 return Ok((Arc::new(store), path));
685 }
686 let url = uri_to_url(uri)?;
687
688 let store = if use_registry_cache {
689 registry.get_store(url.clone(), params).await?
690 } else {
691 registry.new_store(url.clone(), params).await?
692 };
693 let provider = registry.get_provider(url.scheme()).expect_ok()?;
695 let path = provider.extract_path(&url)?;
696
697 Ok((store, path))
698 }
699
700 pub fn extract_path_from_uri(registry: Arc<ObjectStoreRegistry>, uri: &str) -> Result<Path> {
714 let url = uri_to_url(uri)?;
715 let provider = registry
716 .get_provider(url.scheme())
717 .ok_or_else(|| Error::invalid_input(format!("Unknown scheme: {}", url.scheme())))?;
718 provider.extract_path(&url)
719 }
720
721 #[deprecated(note = "Use `from_uri` instead")]
722 pub fn from_path(str_path: &str) -> Result<(Arc<Self>, Path)> {
723 Self::from_uri_and_params(
724 Arc::new(ObjectStoreRegistry::default()),
725 str_path,
726 &Default::default(),
727 )
728 .now_or_never()
729 .unwrap()
730 }
731
732 pub fn local() -> Self {
734 let provider = FileStoreProvider;
735 provider
736 .new_store(Url::parse("file:///").unwrap(), &Default::default())
737 .now_or_never()
738 .unwrap()
739 .unwrap()
740 }
741
742 pub fn memory() -> Self {
744 let provider = MemoryStoreProvider;
745 provider
746 .new_store(Url::parse("memory:///").unwrap(), &Default::default())
747 .now_or_never()
748 .unwrap()
749 .unwrap()
750 }
751
752 pub fn is_local(&self) -> bool {
754 self.scheme == "file" || self.scheme == "file+uring"
755 }
756
757 pub fn has_direct_local_paths(&self) -> bool {
762 self.is_local() && self.store_prefix == self.scheme
763 }
764
765 pub fn is_cloud(&self) -> bool {
766 if self.is_local() || self.scheme == "memory" || self.scheme == "shared-memory" {
767 return false;
768 }
769 true
770 }
771
772 pub fn prefers_lite_scheduler(&self) -> bool {
777 self.scheme == "file+uring"
778 }
779
780 pub fn scheme(&self) -> &str {
781 &self.scheme
782 }
783
784 pub fn block_size(&self) -> usize {
785 self.block_size
786 }
787
788 pub fn max_iop_size(&self) -> u64 {
789 self.max_iop_size
790 }
791
792 pub fn io_parallelism(&self) -> usize {
799 std::env::var("LANCE_IO_THREADS")
800 .map(|val| val.parse::<usize>().unwrap())
801 .unwrap_or(self.io_parallelism)
802 .max(1)
803 }
804
805 pub fn io_tracker(&self) -> &IOTracker {
810 &self.io_tracker
811 }
812
813 pub fn io_stats_snapshot(&self) -> IoStats {
818 self.io_tracker.stats()
819 }
820
821 pub fn io_stats_incremental(&self) -> IoStats {
827 self.io_tracker.incremental_stats()
828 }
829
830 pub fn apply_wrapper(&mut self, wrapper: &dyn WrappingObjectStore) {
836 self.inner = wrapper.wrap(&self.store_prefix, self.inner.clone());
837 self.paginated_lister = self
838 .paginated_lister
839 .take()
840 .and_then(|lister| wrapper.wrap_paginated(&self.store_prefix, lister));
841 }
842
843 pub async fn open(&self, path: &Path) -> Result<Box<dyn Reader>> {
848 match self.scheme.as_str() {
849 "file" if self.has_direct_local_paths() => {
850 LocalObjectReader::open_with_tracker(
851 path,
852 self.block_size,
853 None,
854 Arc::new(self.io_tracker.clone()),
855 )
856 .await
857 }
858 #[cfg(target_os = "linux")]
859 "file+uring" => {
860 let use_current_thread = std::env::var("LANCE_URING_CURRENT_THREAD")
862 .map(|v| str_is_truthy(&v))
863 .unwrap_or(false);
864
865 if use_current_thread {
866 UringCurrentThreadReader::open(
867 path,
868 self.block_size,
869 None,
870 Arc::new(self.io_tracker.clone()),
871 )
872 .await
873 } else {
874 UringReader::open(
875 path,
876 self.block_size,
877 None,
878 Arc::new(self.io_tracker.clone()),
879 )
880 .await
881 }
882 }
883 _ => Ok(Box::new(
884 CloudObjectReader::new(
885 self.inner.clone(),
886 path.clone(),
887 self.block_size,
888 None,
889 self.download_retry_count,
890 )?
891 .with_io_parallelism(self.io_parallelism()),
892 )),
893 }
894 }
895
896 pub async fn open_with_size(&self, path: &Path, known_size: usize) -> Result<Box<dyn Reader>> {
902 if known_size <= self.block_size {
905 return Ok(Box::new(SmallReader::new(
906 self.inner.clone(),
907 path.clone(),
908 self.download_retry_count,
909 known_size,
910 )));
911 }
912
913 match self.scheme.as_str() {
914 "file" if self.has_direct_local_paths() => {
915 LocalObjectReader::open_with_tracker(
916 path,
917 self.block_size,
918 Some(known_size),
919 Arc::new(self.io_tracker.clone()),
920 )
921 .await
922 }
923 #[cfg(target_os = "linux")]
924 "file+uring" => {
925 let use_current_thread = std::env::var("LANCE_URING_CURRENT_THREAD")
927 .map(|v| str_is_truthy(&v))
928 .unwrap_or(false);
929
930 if use_current_thread {
931 UringCurrentThreadReader::open(
932 path,
933 self.block_size,
934 Some(known_size),
935 Arc::new(self.io_tracker.clone()),
936 )
937 .await
938 } else {
939 UringReader::open(
940 path,
941 self.block_size,
942 Some(known_size),
943 Arc::new(self.io_tracker.clone()),
944 )
945 .await
946 }
947 }
948 _ => Ok(Box::new(
949 CloudObjectReader::new(
950 self.inner.clone(),
951 path.clone(),
952 self.block_size,
953 Some(known_size),
954 self.download_retry_count,
955 )?
956 .with_io_parallelism(self.io_parallelism()),
957 )),
958 }
959 }
960
961 pub async fn create_local_writer(path: &std::path::Path) -> Result<ObjectWriter> {
963 let object_store = Self::local();
964 let absolute_path = expand_path(path.to_string_lossy())?;
965 let os_path = Path::from_absolute_path(absolute_path)?;
966 ObjectWriter::new(&object_store, &os_path).await
967 }
968
969 pub async fn open_local(path: &std::path::Path) -> Result<Box<dyn Reader>> {
971 let object_store = Self::local();
972 let absolute_path = expand_path(path.to_string_lossy())?;
973 let os_path = Path::from_absolute_path(absolute_path)?;
974 object_store.open(&os_path).await
975 }
976
977 pub async fn create(&self, path: &Path) -> Result<Box<dyn Writer>> {
979 match self.scheme.as_str() {
980 "file" if self.has_direct_local_paths() => {
981 let local_path = super::local::to_local_path(path);
982 let local_path = std::path::PathBuf::from(&local_path);
983 if let Some(parent) = local_path.parent() {
984 tokio::fs::create_dir_all(parent).await?;
985 }
986 let parent = local_path
987 .parent()
988 .expect("file path must have parent")
989 .to_owned();
990 let named_temp = tokio::task::spawn_blocking(move || {
991 #[cfg(unix)]
992 {
993 tempfile::Builder::new()
995 .permissions(std::fs::Permissions::from_mode(0o666))
996 .tempfile_in(parent)
997 }
998 #[cfg(not(unix))]
999 tempfile::NamedTempFile::new_in(parent)
1000 })
1001 .await
1002 .map_err(|e| Error::io(format!("spawn_blocking failed: {}", e)))??;
1003 let (std_file, temp_path) = named_temp.into_parts();
1004 let file = tokio::fs::File::from_std(std_file);
1005 Ok(Box::new(LocalWriter::new(
1006 file,
1007 path.clone(),
1008 temp_path,
1009 Arc::new(self.io_tracker.clone()),
1010 )))
1011 }
1012 _ => Ok(Box::new(ObjectWriter::new(self, path).await?)),
1013 }
1014 }
1015
1016 pub async fn put(&self, path: &Path, content: &[u8]) -> Result<WriteResult> {
1018 let mut writer = self.create(path).await?;
1019 writer.write_all(content).await?;
1020 Writer::shutdown(writer.as_mut()).await
1021 }
1022
1023 pub async fn put_if_absent(
1032 &self,
1033 path: &Path,
1034 content: PutPayload,
1035 ) -> object_store::Result<()> {
1036 if self.scheme == "cos" {
1037 return Err(object_store::Error::NotSupported {
1038 source: "Tencent COS does not reliably enforce put-if-absent after bucket \
1039 versioning has ever been enabled"
1040 .into(),
1041 });
1042 }
1043
1044 if self.is_local() {
1045 let staging_path =
1046 Path::from(format!("{}.tmp.{}", path, uuid::Uuid::new_v4().simple()));
1047 self.inner.put(&staging_path, content).await?;
1048 let result = self.inner.rename_if_not_exists(&staging_path, path).await;
1049 if result.is_err()
1050 && let Err(error) = self.inner.delete(&staging_path).await
1051 {
1052 log::warn!(
1053 "Failed to remove staging object {} after atomic create failed: {}",
1054 staging_path,
1055 error
1056 );
1057 }
1058 result
1059 } else {
1060 self.inner
1061 .put_opts(
1062 path,
1063 content,
1064 PutOptions {
1065 mode: PutMode::Create,
1066 ..Default::default()
1067 },
1068 )
1069 .await
1070 .map(|_| ())
1071 }
1072 }
1073
1074 pub async fn delete(&self, path: &Path) -> Result<()> {
1075 self.inner.delete(path).await?;
1076 Ok(())
1077 }
1078
1079 const MAX_SINGLE_COPY_BYTES: u64 = 5 * 1024 * 1024 * 1024; pub async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
1084 let multipart_copy_fallback = matches!(self.scheme.as_str(), "s3" | "s3+ddb" | "gs");
1090 self.copy_impl(
1091 from,
1092 to,
1093 multipart_copy_fallback,
1094 Self::MAX_SINGLE_COPY_BYTES,
1095 )
1096 .await
1097 }
1098
1099 pub async fn copy_bulk(
1123 &self,
1124 source_path: &Path,
1125 destination_store: &Self,
1126 destination_path: &Path,
1127 ) -> Result<WriteResult> {
1128 self.copy_bulk_with_server_side_copy(
1129 source_path,
1130 destination_store,
1131 destination_path,
1132 self.uses_server_side_copy(destination_store),
1133 )
1134 .await
1135 }
1136
1137 fn uses_server_side_copy(&self, destination_store: &Self) -> bool {
1138 parse_env_as_bool(SERVER_SIDE_COPY_ENABLED_ENV, false)
1139 && self.can_server_side_copy_to(destination_store)
1140 }
1141
1142 async fn copy_bulk_with_server_side_copy(
1143 &self,
1144 source_path: &Path,
1145 destination_store: &Self,
1146 destination_path: &Path,
1147 server_side_copy_enabled: bool,
1148 ) -> Result<WriteResult> {
1149 if !server_side_copy_enabled || !self.can_server_side_copy_to(destination_store) {
1150 return self
1151 .copy_via_stream(source_path, destination_store, destination_path)
1152 .await;
1153 }
1154
1155 let source_size = self.size(source_path).await?;
1156 let result_size = usize::try_from(source_size).map_err(|source| {
1157 Error::io(format!(
1158 "server-side copy source size conversion failed from {source_path} to \
1159 {destination_path}: source_size={source_size}, error={source}"
1160 ))
1161 })?;
1162 destination_store
1163 .copy(source_path, destination_path)
1164 .await?;
1165 let destination_size = destination_store.size(destination_path).await?;
1166 if destination_size != source_size {
1167 return Err(Error::io(format!(
1168 "server-side copy destination size mismatch from {source_path} to \
1169 {destination_path}: source_size={source_size}, \
1170 destination_size={destination_size}"
1171 )));
1172 }
1173
1174 Ok(WriteResult {
1175 size: result_size,
1176 e_tag: None,
1177 })
1178 }
1179
1180 fn can_server_side_copy_to(&self, destination_store: &Self) -> bool {
1181 self.is_cloud()
1184 && destination_store.is_cloud()
1185 && Arc::ptr_eq(&self.inner, &destination_store.inner)
1186 }
1187
1188 #[instrument(
1211 name = "multipart_stream_copy",
1212 level = "info",
1213 skip(self, source_path, destination_store, destination_path),
1214 fields(
1215 source = %source_path,
1216 destination = %destination_path,
1217 source_size = Empty,
1218 read_chunk_size = Empty,
1219 multipart_part_size = crate::object_writer::initial_upload_size(),
1220 multipart_concurrency = crate::object_writer::max_upload_parallelism(),
1221 part_count = Empty,
1222 bytes_transferred = Empty,
1223 destination_size = Empty,
1224 validation = Empty,
1225 elapsed_ms = Empty,
1226 ),
1227 err
1228 )]
1229 pub async fn copy_via_stream(
1230 &self,
1231 source_path: &Path,
1232 destination_store: &Self,
1233 destination_path: &Path,
1234 ) -> Result<WriteResult> {
1235 let started_at = Instant::now();
1236 if self.has_direct_local_paths() && destination_store.has_direct_local_paths() {
1237 let source_size = std::fs::metadata(super::local::to_local_path(source_path))
1238 .map_err(|source| {
1239 let source = if source.kind() == std::io::ErrorKind::NotFound {
1240 Error::not_found(source_path.to_string())
1241 } else {
1242 Error::from(source)
1243 };
1244 stream_copy_error("source metadata", source_path, destination_path, source)
1245 })?
1246 .len();
1247 let source_size = usize::try_from(source_size).map_err(|source| {
1248 stream_copy_error(
1249 "source size conversion",
1250 source_path,
1251 destination_path,
1252 source,
1253 )
1254 })?;
1255 Span::current().record("source_size", source_size as u64);
1256
1257 let metrics = destination_store.io_tracker.begin_io("copy");
1258 let result = super::local::copy_file(source_path, destination_path);
1259 metrics.record(&result, source_size as u64);
1260 result.map_err(|source| {
1261 stream_copy_error(
1262 "local filesystem copy",
1263 source_path,
1264 destination_path,
1265 source,
1266 )
1267 })?;
1268
1269 let destination_size =
1270 destination_store
1271 .size(destination_path)
1272 .await
1273 .map_err(|source| {
1274 stream_copy_error(
1275 "destination validation",
1276 source_path,
1277 destination_path,
1278 source,
1279 )
1280 })?;
1281 Span::current().record("bytes_transferred", source_size as u64);
1282 Span::current().record("destination_size", destination_size);
1283 if destination_size != source_size as u64 {
1284 Span::current().record("validation", "failed");
1285 return Err(Error::io(format!(
1286 "multipart_stream_copy destination size mismatch from {source_path} to \
1287 {destination_path}: source_size={source_size}, \
1288 destination_size={destination_size}"
1289 )));
1290 }
1291
1292 Span::current().record("validation", "passed");
1293 Span::current().record("elapsed_ms", started_at.elapsed().as_millis() as u64);
1294 return Ok(WriteResult {
1295 size: source_size,
1296 e_tag: None,
1297 });
1298 }
1299
1300 let reader = self.open(source_path).await.map_err(|source| {
1301 stream_copy_error("source open", source_path, destination_path, source)
1302 })?;
1303 let source_size = reader.size().await.map_err(|source| {
1304 stream_copy_error("source metadata", source_path, destination_path, source)
1305 })?;
1306 Span::current().record("source_size", source_size as u64);
1307
1308 let mut writer = destination_store
1309 .create(destination_path)
1310 .await
1311 .map_err(|source| {
1312 stream_copy_error(
1313 "destination writer creation",
1314 source_path,
1315 destination_path,
1316 source,
1317 )
1318 })?;
1319 let read_chunk_size = usize::try_from(self.max_iop_size())
1320 .unwrap_or(usize::MAX)
1321 .max(1);
1322 Span::current().record("read_chunk_size", read_chunk_size as u64);
1323 let mut bytes_transferred = 0usize;
1324 if source_size > 0 {
1325 let first_range = 0..read_chunk_size.min(source_size);
1326 let mut current_range = first_range.clone();
1327 let mut current_bytes = reader.get_range(first_range).await.map_err(|source| {
1328 stream_copy_error("source read", source_path, destination_path, source)
1329 })?;
1330
1331 loop {
1332 let expected_bytes = current_range.len();
1333 if current_bytes.len() != expected_bytes {
1334 Span::current().record("validation", "failed");
1335 return Err(Error::io(format!(
1336 "multipart_stream_copy source range size mismatch from {source_path} to \
1337 {destination_path}: range={current_range:?}, \
1338 expected_bytes={expected_bytes}, actual_bytes={}",
1339 current_bytes.len()
1340 )));
1341 }
1342 bytes_transferred = bytes_transferred
1343 .checked_add(current_bytes.len())
1344 .ok_or_else(|| {
1345 Error::io(format!(
1346 "multipart_stream_copy byte count overflow from {source_path} to \
1347 {destination_path}"
1348 ))
1349 })?;
1350
1351 if bytes_transferred == source_size {
1352 writer.write_all(¤t_bytes).await.map_err(|source| {
1353 stream_copy_error(
1354 "destination write",
1355 source_path,
1356 destination_path,
1357 source,
1358 )
1359 })?;
1360 break;
1361 }
1362
1363 let range_end = bytes_transferred
1364 .checked_add(read_chunk_size)
1365 .unwrap_or(source_size)
1366 .min(source_size);
1367 let next_range = bytes_transferred..range_end;
1368 let next_read = reader.get_range(next_range.clone());
1369 let (write_result, next_bytes) =
1370 tokio::join!(writer.write_all(¤t_bytes), next_read);
1371 write_result.map_err(|source| {
1372 stream_copy_error("destination write", source_path, destination_path, source)
1373 })?;
1374 current_bytes = next_bytes.map_err(|source| {
1375 stream_copy_error("source read", source_path, destination_path, source)
1376 })?;
1377 current_range = next_range;
1378 }
1379 }
1380 Span::current().record("bytes_transferred", bytes_transferred as u64);
1381
1382 let write_result = Writer::shutdown(writer.as_mut()).await.map_err(|source| {
1383 stream_copy_error(
1384 "destination completion",
1385 source_path,
1386 destination_path,
1387 source,
1388 )
1389 })?;
1390 if write_result.size != source_size {
1391 Span::current().record("validation", "failed");
1392 return Err(Error::io(format!(
1393 "multipart_stream_copy writer size mismatch from {source_path} to \
1394 {destination_path}: source_size={source_size}, \
1395 writer_size={}",
1396 write_result.size
1397 )));
1398 }
1399
1400 let destination_size =
1401 destination_store
1402 .size(destination_path)
1403 .await
1404 .map_err(|source| {
1405 stream_copy_error(
1406 "destination validation",
1407 source_path,
1408 destination_path,
1409 source,
1410 )
1411 })?;
1412 Span::current().record("destination_size", destination_size);
1413 if destination_size != source_size as u64 {
1414 Span::current().record("validation", "failed");
1415 return Err(Error::io(format!(
1416 "multipart_stream_copy destination size mismatch from {source_path} to \
1417 {destination_path}: source_size={source_size}, \
1418 destination_size={destination_size}"
1419 )));
1420 }
1421
1422 Span::current().record("validation", "passed");
1423 Span::current().record("elapsed_ms", started_at.elapsed().as_millis() as u64);
1424 Ok(write_result)
1425 }
1426
1427 async fn copy_impl(
1433 &self,
1434 from: &Path,
1435 to: &Path,
1436 multipart_copy_fallback: bool,
1437 max_single_copy: u64,
1438 ) -> Result<()> {
1439 if self.has_direct_local_paths() {
1440 let metrics = self.io_tracker.begin_io("copy");
1442 let result = super::local::copy_file(from, to);
1443 metrics.record(&result, 0);
1444 return result;
1445 }
1446 if multipart_copy_fallback {
1447 let reader = self.open(from).await?;
1450 if reader.size().await? as u64 > max_single_copy {
1451 let mut writer = self.create(to).await?;
1452 writer.copy_from_reader(reader.as_ref()).await?;
1453 Writer::shutdown(writer.as_mut()).await?;
1454 return Ok(());
1455 }
1456 }
1457 Ok(self.inner.copy(from, to).await?)
1458 }
1459
1460 pub async fn read_dir(&self, dir_path: impl Into<Path>) -> Result<Vec<String>> {
1465 let path = dir_path.into();
1466 let path = Path::parse(&path)?;
1467 let output = self.inner.list_with_delimiter(Some(&path)).await?;
1468 Ok(output
1469 .common_prefixes
1470 .iter()
1471 .chain(output.objects.iter().map(|o| &o.location))
1472 .filter_map(|s| s.filename().map(|f| f.to_string()))
1473 .collect())
1474 }
1475
1476 pub async fn list_with_delimiter(&self, prefix: Option<&Path>) -> Result<ListResult> {
1483 Ok(self.inner.list_with_delimiter(prefix).await?)
1484 }
1485
1486 pub fn list(
1487 &self,
1488 path: Option<Path>,
1489 ) -> Pin<Box<dyn Stream<Item = Result<ObjectMeta>> + Send>> {
1490 Box::pin(ListRetryStream::new(self.inner.clone(), path, 5).map(|m| m.map_err(|e| e.into())))
1491 }
1492
1493 pub fn read_dir_all<'a, 'b>(
1497 &'a self,
1498 dir_path: impl Into<&'b Path> + Send,
1499 unmodified_since: Option<DateTime<Utc>>,
1500 ) -> BoxStream<'a, Result<ObjectMeta>> {
1501 self.inner.read_dir_all(dir_path, unmodified_since)
1502 }
1503
1504 pub async fn remove_dir_all(&self, dir_path: impl Into<Path>) -> Result<()> {
1506 let path = dir_path.into();
1507 let path = Path::parse(&path)?;
1508
1509 if let Some(local_dir_operations) = &self.local_dir_operations {
1510 let metrics = self.io_tracker.begin_io("delete");
1511 let result = local_dir_operations.remove_dir_all(&path).await;
1512 metrics.record(&result, 0);
1513 return result;
1514 }
1515 if self.has_direct_local_paths() {
1516 let metrics = self.io_tracker.begin_io("delete");
1520 let result = super::local::remove_dir_all(&path);
1521 metrics.record(&result, 0);
1522 return result;
1523 }
1524 let sub_entries = self
1525 .inner
1526 .list(Some(&path))
1527 .map(|m| m.map(|meta| meta.location))
1528 .boxed();
1529 self.inner
1530 .delete_stream(sub_entries)
1531 .try_collect::<Vec<_>>()
1532 .await?;
1533 if self.scheme == "file-object-store" {
1534 return super::local::remove_dir_all(&path);
1537 }
1538 Ok(())
1539 }
1540
1541 pub async fn remove_empty_dirs(
1567 &self,
1568 root_path: impl Into<Path>,
1569 retained_dirs: HashSet<Path>,
1570 verified_dirs: HashSet<Path>,
1571 unmodified_since: Option<DateTime<Utc>>,
1572 ) -> Result<()> {
1573 if !self.has_direct_local_paths() && self.scheme != "file-object-store" {
1574 return Ok(());
1575 }
1576
1577 let path = Path::parse(root_path.into())?;
1578 let metrics = self.io_tracker.begin_io("delete");
1579 let result = tokio::task::spawn_blocking(move || {
1580 super::local::remove_empty_dirs(&path, &retained_dirs, &verified_dirs, unmodified_since)
1581 })
1582 .await
1583 .map_err(|error| Error::io(format!("empty-directory cleanup task failed: {error}")))?;
1584 metrics.record(&result, 0);
1585 result
1586 }
1587
1588 pub fn remove_stream<'a>(
1589 &'a self,
1590 locations: BoxStream<'a, Result<Path>>,
1591 ) -> BoxStream<'a, Result<Path>> {
1592 let store = Arc::clone(&self.inner);
1593 locations
1594 .and_then(move |location| {
1595 let store = Arc::clone(&store);
1596 async move {
1597 store.delete(&location).await?;
1598 Ok(location)
1599 }
1600 })
1601 .boxed()
1602 }
1603
1604 pub async fn exists(&self, path: &Path) -> Result<bool> {
1606 match self.inner.head(path).await {
1607 Ok(_) => Ok(true),
1608 Err(object_store::Error::NotFound { path: _, source: _ }) => Ok(false),
1609 Err(e) => Err(e.into()),
1610 }
1611 }
1612
1613 pub async fn size(&self, path: &Path) -> Result<u64> {
1615 Ok(self.inner.head(path).await?.size)
1616 }
1617
1618 pub async fn read_one_all(&self, path: &Path) -> Result<Bytes> {
1620 let reader = self.open(path).await?;
1621 Ok(reader.get_all().await?)
1622 }
1623
1624 pub async fn read_one_range(&self, path: &Path, range: Range<usize>) -> Result<Bytes> {
1629 let reader = self.open(path).await?;
1630 Ok(reader.get_range(range).await?)
1631 }
1632}
1633
1634#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy)]
1636pub enum LanceConfigKey {
1637 DownloadRetryCount,
1639}
1640
1641impl FromStr for LanceConfigKey {
1642 type Err = Error;
1643
1644 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
1645 match s.to_ascii_lowercase().as_str() {
1646 "download_retry_count" => Ok(Self::DownloadRetryCount),
1647 _ => Err(Error::invalid_input_source(
1648 format!("Invalid LanceConfigKey: {}", s).into(),
1649 )),
1650 }
1651 }
1652}
1653
1654#[derive(Clone, Debug, Default)]
1655pub struct StorageOptions(pub HashMap<String, String>);
1656
1657impl StorageOptions {
1658 pub fn new(options: HashMap<String, String>) -> Self {
1660 let mut options = options;
1661 if let Ok(value) = std::env::var("AZURE_STORAGE_ALLOW_HTTP") {
1662 options.insert("allow_http".into(), value);
1663 }
1664 if let Ok(value) = std::env::var("AZURE_STORAGE_USE_HTTP") {
1665 options.insert("allow_http".into(), value);
1666 }
1667 if let Ok(value) = std::env::var("AWS_ALLOW_HTTP") {
1668 options.insert("allow_http".into(), value);
1669 }
1670 if let Ok(value) = std::env::var("OBJECT_STORE_CLIENT_MAX_RETRIES") {
1671 options.insert("client_max_retries".into(), value);
1672 }
1673 if let Ok(value) = std::env::var("OBJECT_STORE_CLIENT_RETRY_TIMEOUT") {
1674 options.insert("client_retry_timeout".into(), value);
1675 }
1676 Self(options)
1677 }
1678
1679 pub fn allow_http(&self) -> bool {
1681 self.0.iter().any(|(key, value)| {
1682 key.to_ascii_lowercase().contains("allow_http") & str_is_truthy(value)
1683 })
1684 }
1685
1686 pub fn download_retry_count(&self) -> usize {
1688 self.0
1689 .iter()
1690 .find(|(key, _)| key.eq_ignore_ascii_case("download_retry_count"))
1691 .map(|(_, value)| value.parse::<usize>().unwrap_or(3))
1692 .unwrap_or(3)
1693 }
1694
1695 pub fn client_max_retries(&self) -> usize {
1697 self.0
1698 .iter()
1699 .find(|(key, _)| key.eq_ignore_ascii_case("client_max_retries"))
1700 .and_then(|(_, value)| value.parse::<usize>().ok())
1701 .unwrap_or(3)
1702 }
1703
1704 pub fn client_retry_timeout(&self) -> u64 {
1706 self.0
1707 .iter()
1708 .find(|(key, _)| key.eq_ignore_ascii_case("client_retry_timeout"))
1709 .and_then(|(_, value)| value.parse::<u64>().ok())
1710 .unwrap_or(180)
1711 }
1712
1713 pub fn get(&self, key: &str) -> Option<&String> {
1714 self.0.get(key)
1715 }
1716
1717 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1725 pub fn client_options(&self) -> Result<ClientOptions> {
1726 let mut headers = HeaderMap::new();
1727 for (key, value) in &self.0 {
1728 if let Some(header_name) = key.strip_prefix("headers.") {
1729 let name = header_name
1730 .parse::<http::header::HeaderName>()
1731 .map_err(|e| {
1732 Error::invalid_input(format!("invalid header name '{header_name}': {e}"))
1733 })?;
1734 let val = HeaderValue::from_str(value).map_err(|e| {
1735 Error::invalid_input(format!("invalid header value for '{header_name}': {e}"))
1736 })?;
1737 headers.insert(name, val);
1738 }
1739 }
1740 let mut client_options = ClientOptions::default();
1741 if !headers.is_empty() {
1742 client_options = client_options.with_default_headers(headers);
1743 }
1744 Ok(client_options)
1745 }
1746
1747 pub fn expires_at_millis(&self) -> Option<u64> {
1749 self.0
1750 .get(EXPIRES_AT_MILLIS_KEY)
1751 .and_then(|s| s.parse::<u64>().ok())
1752 }
1753}
1754
1755impl From<HashMap<String, String>> for StorageOptions {
1756 fn from(value: HashMap<String, String>) -> Self {
1757 Self::new(value)
1758 }
1759}
1760
1761static DEFAULT_OBJECT_STORE_REGISTRY: std::sync::LazyLock<ObjectStoreRegistry> =
1762 std::sync::LazyLock::new(ObjectStoreRegistry::default);
1763
1764impl ObjectStore {
1765 #[allow(clippy::too_many_arguments)]
1766 pub fn new(
1767 mut store: Arc<DynObjectStore>,
1768 location: Url,
1769 block_size: Option<usize>,
1770 wrapper: Option<Arc<dyn WrappingObjectStore>>,
1771 use_constant_size_upload_parts: bool,
1772 list_is_lexically_ordered: bool,
1773 io_parallelism: usize,
1774 download_retry_count: usize,
1775 storage_options: Option<&HashMap<String, String>>,
1776 ) -> Self {
1777 let scheme = location.scheme();
1778 let block_size = block_size.unwrap_or_else(|| infer_block_size(scheme));
1779 let store_prefix = match DEFAULT_OBJECT_STORE_REGISTRY.get_provider(scheme) {
1780 Some(provider) => provider
1781 .calculate_object_store_prefix(&location, storage_options)
1782 .unwrap(),
1783 None => {
1784 let store_prefix = format!("{}${}", location.scheme(), location.authority());
1785 log::warn!(
1786 "Guessing that object store prefix is {}, since object store scheme is not found in registry.",
1787 store_prefix
1788 );
1789 store_prefix
1790 }
1791 };
1792 let mut io_tracker = IOTracker::default();
1793 meter_store(&mut store, &mut io_tracker, &store_prefix);
1794
1795 let store = match wrapper {
1796 Some(wrapper) => wrapper.wrap(&store_prefix, store),
1797 None => store,
1798 };
1799
1800 let tracked_store = io_tracker.wrap("", store);
1802
1803 Self {
1804 inner: tracked_store,
1805 local_dir_operations: None,
1806 scheme: scheme.into(),
1807 block_size,
1808 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
1809 use_constant_size_upload_parts,
1810 list_is_lexically_ordered,
1811 io_parallelism,
1812 download_retry_count,
1813 io_tracker,
1814 store_prefix,
1815 paginated_lister: None,
1817 }
1818 }
1819}
1820
1821#[cfg(feature = "metrics")]
1830fn meter_store(inner: &mut Arc<dyn OSObjectStore>, io_tracker: &mut IOTracker, store_prefix: &str) {
1831 use crate::object_store::metrics::ObjectStoreMetricsExt;
1832 io_tracker.set_metrics_base(store_prefix);
1833 *inner = inner.clone().metered(store_prefix.to_owned());
1834}
1835
1836#[cfg(not(feature = "metrics"))]
1837fn meter_store(
1838 _inner: &mut Arc<dyn OSObjectStore>,
1839 _io_tracker: &mut IOTracker,
1840 _store_prefix: &str,
1841) {
1842}
1843
1844fn infer_block_size(scheme: &str) -> usize {
1845 match scheme {
1849 "file" => 4 * 1024,
1850 _ => 64 * 1024,
1851 }
1852}
1853
1854#[cfg(test)]
1855mod tests {
1856 use super::*;
1857 use async_trait::async_trait;
1858 use bytes::Bytes;
1859 use lance_core::utils::tempfile::{TempStdDir, TempStdFile, TempStrDir};
1860 use object_store::memory::InMemory;
1861 use object_store::{
1862 CopyOptions, GetOptions, GetResult, ListResult, MultipartUpload, PutMultipartOptions,
1863 PutOptions, PutPayload, PutResult, Result as OSResult, UploadPart,
1864 };
1865 use rstest::rstest;
1866 use serial_test::serial;
1867 use std::env::set_current_dir;
1868 use std::fmt::{Display, Formatter};
1869 use std::fs::{create_dir_all, write};
1870 use std::ops::Range;
1871 use std::path::Path as StdPath;
1872 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1873
1874 fn write_to_file(path_str: &str, contents: &str) -> std::io::Result<()> {
1876 let path = expand_path(path_str).map_err(std::io::Error::other)?;
1877 std::fs::create_dir_all(path.parent().unwrap())?;
1878 write(path, contents)
1879 }
1880
1881 async fn read_from_store(store: &ObjectStore, path: &Path) -> Result<String> {
1882 let test_file_store = store.open(path).await.unwrap();
1883 let size = test_file_store.size().await.unwrap();
1884 let bytes = test_file_store.get_range(0..size).await.unwrap();
1885 let contents = String::from_utf8(bytes.to_vec()).unwrap();
1886 Ok(contents)
1887 }
1888
1889 #[tokio::test]
1890 async fn test_put_if_absent() {
1891 let temp_dir = TempStrDir::default();
1892 let path = Path::from(format!("{}/atomic-create", temp_dir.as_str()));
1893 let store = ObjectStore::local();
1894 store
1895 .put_if_absent(&path, Bytes::from_static(b"first").into())
1896 .await
1897 .unwrap();
1898 let error = store
1899 .put_if_absent(&path, Bytes::from_static(b"second").into())
1900 .await
1901 .unwrap_err();
1902 assert!(matches!(
1903 error,
1904 object_store::Error::AlreadyExists { .. } | object_store::Error::Precondition { .. }
1905 ));
1906 assert_eq!(
1907 store.read_one_all(&path).await.unwrap(),
1908 b"first".as_slice()
1909 );
1910 }
1911
1912 #[tokio::test]
1913 async fn test_put_if_absent_rejects_cos() {
1914 let mut store = ObjectStore::memory();
1915 store.scheme = "cos".to_string();
1916 let path = Path::from("atomic-create");
1917
1918 let error = store
1919 .put_if_absent(&path, Bytes::from_static(b"value").into())
1920 .await
1921 .unwrap_err();
1922
1923 assert!(matches!(error, object_store::Error::NotSupported { .. }));
1924 assert!(!store.exists(&path).await.unwrap());
1925 }
1926
1927 #[tokio::test]
1928 async fn test_io_parallelism_clamped_to_nonzero() {
1929 let store = ObjectStore::local();
1932 let mem_store = ObjectStore::memory();
1935 let path = Path::from("/io_parallelism_probe");
1936 mem_store.put(&path, b"x").await.unwrap();
1937
1938 unsafe { std::env::set_var("LANCE_IO_THREADS", "0") };
1941 assert_eq!(
1942 store.io_parallelism(),
1943 1,
1944 "LANCE_IO_THREADS=0 must clamp to 1"
1945 );
1946 assert_eq!(
1947 mem_store.open(&path).await.unwrap().io_parallelism(),
1948 1,
1949 "an opened reader must report the store's clamped parallelism"
1950 );
1951
1952 unsafe { std::env::set_var("LANCE_IO_THREADS", "8") };
1953 assert_eq!(
1954 store.io_parallelism(),
1955 8,
1956 "a positive override must pass through unchanged"
1957 );
1958 assert_eq!(
1959 mem_store.open(&path).await.unwrap().io_parallelism(),
1960 8,
1961 "an opened reader must honor the configured request limit"
1962 );
1963 assert_eq!(
1964 mem_store
1965 .open_with_size(&path, 1024 * 1024)
1966 .await
1967 .unwrap()
1968 .io_parallelism(),
1969 8,
1970 "a sized reader must honor the configured request limit"
1971 );
1972
1973 unsafe { std::env::remove_var("LANCE_IO_THREADS") };
1974 assert!(
1975 store.io_parallelism() >= 1,
1976 "the configured default parallelism must be at least 1"
1977 );
1978 }
1979
1980 #[tokio::test]
1981 async fn test_absolute_paths() {
1982 let tmp_path = TempStrDir::default();
1983 write_to_file(
1984 &format!("{tmp_path}/bar/foo.lance/test_file"),
1985 "TEST_CONTENT",
1986 )
1987 .unwrap();
1988
1989 for uri in &[
1991 format!("{tmp_path}/bar/foo.lance"),
1992 format!("{tmp_path}/./bar/foo.lance"),
1993 format!("{tmp_path}/bar/foo.lance/../foo.lance"),
1994 ] {
1995 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
1996 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
1997 .await
1998 .unwrap();
1999 assert_eq!(contents, "TEST_CONTENT");
2000 }
2001 }
2002
2003 #[tokio::test]
2004 async fn test_cloud_paths() {
2005 let uri = "s3://bucket/foo.lance";
2006 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
2007 assert_eq!(store.scheme, "s3");
2008 assert_eq!(path.to_string(), "foo.lance");
2009
2010 let (store, path) = ObjectStore::from_uri("s3+ddb://bucket/foo.lance")
2011 .await
2012 .unwrap();
2013 assert_eq!(store.scheme, "s3");
2014 assert_eq!(path.to_string(), "foo.lance");
2015
2016 let (store, path) = ObjectStore::from_uri("gs://bucket/foo.lance")
2017 .await
2018 .unwrap();
2019 assert_eq!(store.scheme, "gs");
2020 assert_eq!(path.to_string(), "foo.lance");
2021
2022 let (store, path) =
2023 ObjectStore::from_uri("abfss://filesystem@account.dfs.core.windows.net/foo.lance")
2024 .await
2025 .unwrap();
2026 assert_eq!(store.scheme, "abfss");
2027 assert_eq!(path.to_string(), "foo.lance");
2028 }
2029
2030 async fn test_block_size_used_test_helper(
2031 uri: &str,
2032 storage_options: Option<HashMap<String, String>>,
2033 default_expected_block_size: usize,
2034 ) {
2035 let registry = Arc::new(ObjectStoreRegistry::default());
2037 let accessor = storage_options
2038 .clone()
2039 .map(|opts| Arc::new(StorageOptionsAccessor::with_static_options(opts)));
2040 let params = ObjectStoreParams {
2041 storage_options_accessor: accessor.clone(),
2042 ..ObjectStoreParams::default()
2043 };
2044 let (store, _) = ObjectStore::from_uri_and_params(registry, uri, ¶ms)
2045 .await
2046 .unwrap();
2047 assert_eq!(store.block_size, default_expected_block_size);
2048
2049 let registry = Arc::new(ObjectStoreRegistry::default());
2051 let params = ObjectStoreParams {
2052 block_size: Some(1024),
2053 storage_options_accessor: accessor,
2054 ..ObjectStoreParams::default()
2055 };
2056 let (store, _) = ObjectStore::from_uri_and_params(registry, uri, ¶ms)
2057 .await
2058 .unwrap();
2059 assert_eq!(store.block_size, 1024);
2060 }
2061
2062 #[rstest]
2063 #[case("s3://bucket/foo.lance", None)]
2064 #[case("gs://bucket/foo.lance", None)]
2065 #[case("az://account/bucket/foo.lance",
2066 Some(HashMap::from([
2067 (String::from("account_name"), String::from("account")),
2068 (String::from("container_name"), String::from("container"))
2069 ])))]
2070 #[case("abfss://filesystem@account.dfs.core.windows.net/foo.lance",
2071 Some(HashMap::from([
2072 (String::from("account_name"), String::from("account")),
2073 (String::from("container_name"), String::from("filesystem"))
2074 ])))]
2075 #[tokio::test]
2076 async fn test_block_size_used_cloud(
2077 #[case] uri: &str,
2078 #[case] storage_options: Option<HashMap<String, String>>,
2079 ) {
2080 test_block_size_used_test_helper(uri, storage_options, 64 * 1024).await;
2081 }
2082
2083 #[rstest]
2084 #[case("file")]
2085 #[case("file-object-store")]
2086 #[case("memory:///bucket/foo.lance")]
2087 #[tokio::test]
2088 async fn test_block_size_used_file(#[case] prefix: &str) {
2089 let tmp_path = TempStrDir::default();
2090 let path = format!("{tmp_path}/bar/foo.lance/test_file");
2091 write_to_file(&path, "URL").unwrap();
2092 let uri = format!("{prefix}:///{path}");
2093 test_block_size_used_test_helper(&uri, None, 4 * 1024).await;
2094 }
2095
2096 #[tokio::test]
2097 async fn test_relative_paths() {
2098 let tmp_path = TempStrDir::default();
2099 write_to_file(
2100 &format!("{tmp_path}/bar/foo.lance/test_file"),
2101 "RELATIVE_URL",
2102 )
2103 .unwrap();
2104
2105 set_current_dir(StdPath::new(tmp_path.as_ref())).expect("Error changing current dir");
2106 let (store, path) = ObjectStore::from_uri("./bar/foo.lance").await.unwrap();
2107
2108 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
2109 .await
2110 .unwrap();
2111 assert_eq!(contents, "RELATIVE_URL");
2112 }
2113
2114 #[tokio::test]
2115 async fn test_tilde_expansion() {
2116 let uri = "~/foo.lance";
2117 write_to_file(&format!("{uri}/test_file"), "TILDE").unwrap();
2118 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
2119 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
2120 .await
2121 .unwrap();
2122 assert_eq!(contents, "TILDE");
2123 }
2124
2125 #[tokio::test]
2126 async fn test_read_directory() {
2127 let path = TempStdDir::default();
2128 create_dir_all(path.join("foo").join("bar")).unwrap();
2129 create_dir_all(path.join("foo").join("zoo")).unwrap();
2130 create_dir_all(path.join("foo").join("zoo").join("abc")).unwrap();
2131 write_to_file(
2132 path.join("foo").join("test_file").to_str().unwrap(),
2133 "read_dir",
2134 )
2135 .unwrap();
2136 let (store, base) = ObjectStore::from_uri(path.to_str().unwrap()).await.unwrap();
2137
2138 let sub_dirs = store.read_dir(base.clone().join("foo")).await.unwrap();
2139 assert_eq!(sub_dirs, vec!["bar", "zoo", "test_file"]);
2140 }
2141
2142 #[tokio::test]
2143 async fn test_delete_directory_local_store() {
2144 test_delete_directory("").await;
2145 }
2146
2147 #[tokio::test]
2148 async fn test_delete_directory_file_object_store() {
2149 test_delete_directory("file-object-store").await;
2150 }
2151
2152 async fn test_delete_directory(scheme: &str) {
2153 let path = TempStdDir::default();
2154 create_dir_all(path.join("foo").join("bar")).unwrap();
2155 create_dir_all(path.join("foo").join("zoo")).unwrap();
2156 create_dir_all(path.join("foo").join("zoo").join("abc")).unwrap();
2157 write_to_file(
2158 path.join("foo")
2159 .join("bar")
2160 .join("test_file")
2161 .to_str()
2162 .unwrap(),
2163 "delete",
2164 )
2165 .unwrap();
2166 let file_url = Url::from_directory_path(&path).unwrap();
2167 let url = if scheme.is_empty() {
2168 file_url
2169 } else {
2170 let mut url = Url::parse(&format!("{scheme}:///")).unwrap();
2171 url.set_path(file_url.path());
2173 url
2174 };
2175 let (store, base) = ObjectStore::from_uri(url.as_ref()).await.unwrap();
2176 store
2177 .remove_dir_all(base.clone().join("foo"))
2178 .await
2179 .unwrap();
2180
2181 assert!(!path.join("foo").exists());
2182 }
2183
2184 #[rstest]
2185 #[case("file")]
2186 #[case("file-object-store")]
2187 #[tokio::test]
2188 async fn test_remove_empty_directories(#[case] scheme: &str) {
2189 let path = TempStdDir::default();
2190 let stale_dir = path.join("stale");
2191 let nested_stale_dir = path.join("nested_stale");
2192 let nested_stale_child = nested_stale_dir.join("child");
2193 create_dir_all(&stale_dir).unwrap();
2194 create_dir_all(&nested_stale_child).unwrap();
2195 create_dir_all(path.join("retained").join("child")).unwrap();
2196 write_to_file(
2197 path.join("file_bearing")
2198 .join("test_file")
2199 .to_str()
2200 .unwrap(),
2201 "keep",
2202 )
2203 .unwrap();
2204 create_dir_all(path.join("file_bearing").join("empty_child")).unwrap();
2205
2206 let file_url = Url::from_directory_path(&path).unwrap();
2207 let mut url = Url::parse(&format!("{scheme}:///")).unwrap();
2208 url.set_path(file_url.path());
2209 let (store, base) = ObjectStore::from_uri(url.as_ref()).await.unwrap();
2210
2211 #[cfg(unix)]
2212 let unmodified_since = {
2213 let old_modified_time =
2214 std::time::SystemTime::now() - std::time::Duration::from_secs(10 * 24 * 60 * 60);
2215 for directory in [&stale_dir, &nested_stale_dir, &nested_stale_child] {
2216 std::fs::File::open(directory)
2217 .unwrap()
2218 .set_times(std::fs::FileTimes::new().set_modified(old_modified_time))
2219 .unwrap();
2220 }
2221 DateTime::<Utc>::from(std::time::SystemTime::now())
2222 - chrono::TimeDelta::try_days(7).unwrap()
2223 };
2224 #[cfg(not(unix))]
2225 let unmodified_since = DateTime::<Utc>::from(std::time::SystemTime::now())
2226 + chrono::TimeDelta::try_days(1).unwrap();
2227
2228 store
2229 .remove_empty_dirs(
2230 base.clone(),
2231 HashSet::from([base.clone().join("retained")]),
2232 HashSet::new(),
2233 Some(unmodified_since),
2234 )
2235 .await
2236 .unwrap();
2237
2238 assert!(!path.join("stale").exists());
2239 assert!(!path.join("nested_stale").exists());
2240 assert!(path.join("retained").join("child").exists());
2241 assert!(path.join("file_bearing").join("empty_child").exists());
2242
2243 create_dir_all(path.join("fresh")).unwrap();
2244 create_dir_all(path.join("verified")).unwrap();
2245 store
2246 .remove_empty_dirs(
2247 base.clone(),
2248 HashSet::from([base.clone().join("retained")]),
2249 HashSet::from([base.clone().join("verified")]),
2250 Some(
2251 DateTime::<Utc>::from(std::time::SystemTime::now())
2252 - chrono::TimeDelta::try_days(7).unwrap(),
2253 ),
2254 )
2255 .await
2256 .unwrap();
2257
2258 assert!(path.join("fresh").exists());
2259 assert!(!path.join("verified").exists());
2260 }
2261
2262 #[derive(Debug)]
2263 struct TestWrapper {
2264 called: AtomicBool,
2265
2266 return_value: Arc<dyn OSObjectStore>,
2267 }
2268
2269 impl WrappingObjectStore for TestWrapper {
2270 fn wrap(
2271 &self,
2272 _store_prefix: &str,
2273 _original: Arc<dyn OSObjectStore>,
2274 ) -> Arc<dyn OSObjectStore> {
2275 self.called.store(true, Ordering::Relaxed);
2276
2277 self.return_value.clone()
2279 }
2280
2281 fn wrap_paginated(
2284 &self,
2285 _store_prefix: &str,
2286 _original: Arc<dyn PaginatedListStore>,
2287 ) -> Option<Arc<dyn PaginatedListStore>> {
2288 None
2289 }
2290 }
2291
2292 impl TestWrapper {
2293 fn called(&self) -> bool {
2294 self.called.load(Ordering::Relaxed)
2295 }
2296 }
2297
2298 #[derive(Debug)]
2300 struct StubLister;
2301
2302 #[async_trait]
2303 impl PaginatedListStore for StubLister {
2304 async fn list_paginated(
2305 &self,
2306 _prefix: Option<&str>,
2307 _opts: object_store::list::PaginatedListOptions,
2308 ) -> object_store::Result<object_store::list::PaginatedListResult> {
2309 unimplemented!("this lister exists to be wrapped, not to list")
2310 }
2311 }
2312
2313 #[derive(Debug)]
2315 struct PaginatedTestWrapper {
2316 name: &'static str,
2317 log: Arc<std::sync::Mutex<Vec<String>>>,
2318 }
2319
2320 impl WrappingObjectStore for PaginatedTestWrapper {
2321 fn wrap(
2322 &self,
2323 _store_prefix: &str,
2324 original: Arc<dyn OSObjectStore>,
2325 ) -> Arc<dyn OSObjectStore> {
2326 original
2327 }
2328
2329 fn wrap_paginated(
2330 &self,
2331 store_prefix: &str,
2332 original: Arc<dyn PaginatedListStore>,
2333 ) -> Option<Arc<dyn PaginatedListStore>> {
2334 self.log
2335 .lock()
2336 .unwrap()
2337 .push(format!("{}@{store_prefix}", self.name));
2338 Some(original)
2339 }
2340 }
2341
2342 #[rstest]
2346 #[case::every_wrapper_keeps_it(false, vec!["first@memory", "second@memory"])]
2347 #[case::one_wrapper_gives_it_up(true, vec!["first@memory"])]
2348 fn test_a_chain_wraps_the_lister_until_one_gives_it_up(
2349 #[case] gives_up: bool,
2350 #[case] expected_log: Vec<&str>,
2351 ) {
2352 let log = Arc::new(std::sync::Mutex::new(Vec::new()));
2353 let mut wrappers: Vec<Arc<dyn WrappingObjectStore>> =
2354 vec![Arc::new(PaginatedTestWrapper {
2355 name: "first",
2356 log: log.clone(),
2357 })];
2358 if gives_up {
2359 wrappers.push(Arc::new(TestWrapper {
2360 called: AtomicBool::new(false),
2361 return_value: Arc::new(InMemory::new()),
2362 }));
2363 }
2364 wrappers.push(Arc::new(PaginatedTestWrapper {
2365 name: "second",
2366 log: log.clone(),
2367 }));
2368
2369 let wrapped = ChainedWrappingObjectStore::new(wrappers)
2370 .wrap_paginated("memory", Arc::new(StubLister));
2371
2372 assert_eq!(wrapped.is_none(), gives_up);
2373 assert_eq!(*log.lock().unwrap(), expected_log);
2374 }
2375
2376 #[rstest]
2380 #[case::gives_up_the_pushdown(true)]
2381 #[case::keeps_the_pushdown(false)]
2382 fn test_apply_wrapper_keeps_inner_and_the_lister_in_sync(#[case] gives_up: bool) {
2383 let replacement = Arc::new(InMemory::new());
2384 let giving_up = TestWrapper {
2385 called: AtomicBool::new(false),
2386 return_value: replacement.clone(),
2387 };
2388 let keeping = PaginatedTestWrapper {
2389 name: "passthrough",
2390 log: Arc::new(std::sync::Mutex::new(Vec::new())),
2391 };
2392 let wrapper: &dyn WrappingObjectStore = match gives_up {
2393 true => &giving_up,
2394 false => &keeping,
2395 };
2396
2397 let mut store = ObjectStore::memory();
2398 store.paginated_lister = Some(Arc::new(StubLister) as Arc<dyn PaginatedListStore>);
2399 store.apply_wrapper(wrapper);
2400
2401 assert_eq!(
2402 store.paginated_lister.is_some(),
2403 !gives_up,
2404 "the lister has to follow what the wrapper said"
2405 );
2406 assert_eq!(
2409 Arc::ptr_eq(&store.inner, &(replacement as Arc<dyn OSObjectStore>)),
2410 gives_up
2411 );
2412 }
2413
2414 #[tokio::test]
2415 async fn test_wrapper_identity_is_stable_across_tasks() {
2416 let wrapper = Arc::new(TestWrapper {
2417 called: AtomicBool::new(false),
2418 return_value: Arc::new(InMemory::new()),
2419 });
2420 let initial_params = ObjectStoreParams {
2421 object_store_wrapper: Some(wrapper.clone()),
2422 ..ObjectStoreParams::default()
2423 };
2424 let task_params = tokio::spawn(async move {
2425 ObjectStoreParams {
2426 object_store_wrapper: Some(wrapper),
2427 ..ObjectStoreParams::default()
2428 }
2429 })
2430 .await
2431 .unwrap();
2432
2433 assert_eq!(initial_params, task_params);
2434
2435 let mut initial_hasher = std::hash::DefaultHasher::new();
2436 std::hash::Hash::hash(&initial_params, &mut initial_hasher);
2437 let mut task_hasher = std::hash::DefaultHasher::new();
2438 std::hash::Hash::hash(&task_params, &mut task_hasher);
2439 assert_eq!(
2440 std::hash::Hasher::finish(&initial_hasher),
2441 std::hash::Hasher::finish(&task_hasher)
2442 );
2443 }
2444
2445 #[tokio::test]
2446 async fn test_wrapping_object_store_option_is_used() {
2447 let mock_inner_store: Arc<dyn OSObjectStore> = Arc::new(InMemory::new());
2449 let registry = Arc::new(ObjectStoreRegistry::default());
2450
2451 assert_eq!(Arc::strong_count(&mock_inner_store), 1);
2452
2453 let wrapper = Arc::new(TestWrapper {
2454 called: AtomicBool::new(false),
2455 return_value: mock_inner_store.clone(),
2456 });
2457
2458 let params = ObjectStoreParams {
2459 object_store_wrapper: Some(wrapper.clone()),
2460 ..ObjectStoreParams::default()
2461 };
2462
2463 assert!(!wrapper.called());
2465
2466 let _ = ObjectStore::from_uri_and_params(registry, "memory:///", ¶ms)
2467 .await
2468 .unwrap();
2469
2470 assert!(wrapper.called());
2472
2473 assert_eq!(Arc::strong_count(&mock_inner_store), 2);
2476 }
2477
2478 #[tokio::test]
2479 async fn test_local_paths() {
2480 let file_path = TempStdFile::default();
2481 let mut writer = ObjectStore::create_local_writer(&file_path).await.unwrap();
2482 writer.write_all(b"LOCAL").await.unwrap();
2483 Writer::shutdown(&mut writer).await.unwrap();
2484
2485 let reader = ObjectStore::open_local(&file_path).await.unwrap();
2486 let buf = reader.get_range(0..5).await.unwrap();
2487 assert_eq!(buf.as_ref(), b"LOCAL");
2488 }
2489
2490 #[cfg(unix)]
2491 #[tokio::test]
2492 async fn test_direct_local_writer_uses_standard_file_permissions() {
2493 let directory = TempStdDir::default();
2494 let reference_path = directory.join("reference");
2495 std::fs::File::create(&reference_path).unwrap();
2496 let expected_mode = std::fs::metadata(reference_path)
2497 .unwrap()
2498 .permissions()
2499 .mode()
2500 & 0o777;
2501
2502 let output_path = directory.join("output");
2503 let object_path = Path::from_absolute_path(&output_path).unwrap();
2504 let store = ObjectStore::local();
2505 let mut writer = store.create(&object_path).await.unwrap();
2506 writer.write_all(b"LOCAL").await.unwrap();
2507 Writer::shutdown(writer.as_mut()).await.unwrap();
2508
2509 let actual_mode = std::fs::metadata(output_path).unwrap().permissions().mode() & 0o777;
2510 assert_eq!(actual_mode, expected_mode);
2511 }
2512
2513 #[tokio::test]
2514 async fn test_read_one() {
2515 let file_path = TempStdFile::default();
2516 let mut writer = ObjectStore::create_local_writer(&file_path).await.unwrap();
2517 writer.write_all(b"LOCAL").await.unwrap();
2518 Writer::shutdown(&mut writer).await.unwrap();
2519
2520 let file_path_os = object_store::path::Path::parse(file_path.to_str().unwrap()).unwrap();
2521 let obj_store = ObjectStore::local();
2522 let buf = obj_store.read_one_all(&file_path_os).await.unwrap();
2523 assert_eq!(buf.as_ref(), b"LOCAL");
2524
2525 let buf = obj_store.read_one_range(&file_path_os, 0..5).await.unwrap();
2526 assert_eq!(buf.as_ref(), b"LOCAL");
2527 }
2528
2529 #[tokio::test]
2530 #[cfg(windows)]
2531 async fn test_windows_paths() {
2532 use std::path::Component;
2533 use std::path::Prefix;
2534 use std::path::Prefix::*;
2535
2536 fn get_path_prefix(path: &StdPath) -> Prefix<'_> {
2537 match path.components().next().unwrap() {
2538 Component::Prefix(prefix_component) => prefix_component.kind(),
2539 _ => panic!(),
2540 }
2541 }
2542
2543 fn get_drive_letter(prefix: Prefix) -> String {
2544 match prefix {
2545 Disk(bytes) => String::from_utf8(vec![bytes]).unwrap(),
2546 _ => panic!(),
2547 }
2548 }
2549
2550 let tmp_path = TempStdFile::default();
2551 let prefix = get_path_prefix(&tmp_path);
2552 let drive_letter = get_drive_letter(prefix);
2553
2554 write_to_file(
2555 &(format!("{drive_letter}:/test_folder/test.lance") + "/test_file"),
2556 "WINDOWS",
2557 )
2558 .unwrap();
2559
2560 for uri in &[
2561 format!("{drive_letter}:/test_folder/test.lance"),
2562 format!("{drive_letter}:\\test_folder\\test.lance"),
2563 ] {
2564 let (store, base) = ObjectStore::from_uri(uri).await.unwrap();
2565 let contents = read_from_store(store.as_ref(), &base.clone().join("test_file"))
2566 .await
2567 .unwrap();
2568 assert_eq!(contents, "WINDOWS");
2569 }
2570 }
2571
2572 #[tokio::test]
2573 async fn test_cross_filesystem_copy() {
2574 let source_dir = TempStdDir::default();
2576 let dest_dir = TempStdDir::default();
2577
2578 let source_file_name = "test_file.txt";
2580 let source_file = source_dir.join(source_file_name);
2581 std::fs::write(&source_file, b"test content").unwrap();
2582
2583 let (store, base_path) = ObjectStore::from_uri(source_dir.to_str().unwrap())
2585 .await
2586 .unwrap();
2587
2588 let from_path = base_path.clone().join(source_file_name);
2590
2591 let dest_file = dest_dir.join("copied_file.txt");
2593 let dest_str = dest_file.to_str().unwrap();
2594 let to_path = object_store::path::Path::parse(dest_str).unwrap();
2595
2596 store.copy(&from_path, &to_path).await.unwrap();
2598
2599 assert!(dest_file.exists());
2601 let copied_content = std::fs::read(&dest_file).unwrap();
2602 assert_eq!(copied_content, b"test content");
2603 }
2604
2605 #[tokio::test]
2606 async fn test_copy_creates_parent_directories() {
2607 let source_dir = TempStdDir::default();
2608 let dest_dir = TempStdDir::default();
2609
2610 let source_file_name = "test_file.txt";
2612 let source_file = source_dir.join(source_file_name);
2613 std::fs::write(&source_file, b"test content").unwrap();
2614
2615 let (store, base_path) = ObjectStore::from_uri(source_dir.to_str().unwrap())
2617 .await
2618 .unwrap();
2619
2620 let from_path = base_path.clone().join(source_file_name);
2622
2623 let dest_file = dest_dir.join("nested").join("dirs").join("copied_file.txt");
2625 let dest_str = dest_file.to_str().unwrap();
2626 let to_path = object_store::path::Path::parse(dest_str).unwrap();
2627
2628 store.copy(&from_path, &to_path).await.unwrap();
2630
2631 assert!(dest_file.exists());
2633 assert!(dest_file.parent().unwrap().exists());
2634 let copied_content = std::fs::read(&dest_file).unwrap();
2635 assert_eq!(copied_content, b"test content");
2636 }
2637
2638 #[derive(Debug)]
2643 struct CopyFailingStore {
2644 inner: InMemory,
2645 }
2646
2647 impl Display for CopyFailingStore {
2648 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2649 write!(f, "CopyFailingStore")
2650 }
2651 }
2652
2653 #[derive(Debug, Default)]
2654 struct MultipartObservations {
2655 part_count: AtomicUsize,
2656 abort_count: AtomicUsize,
2657 native_copy_count: AtomicUsize,
2658 }
2659
2660 #[derive(Debug)]
2661 struct ObservedMultipartUpload {
2662 inner: Box<dyn MultipartUpload>,
2663 observations: Arc<MultipartObservations>,
2664 fail_parts: bool,
2665 }
2666
2667 #[async_trait]
2668 impl MultipartUpload for ObservedMultipartUpload {
2669 fn put_part(&mut self, data: PutPayload) -> UploadPart {
2670 self.observations.part_count.fetch_add(1, Ordering::SeqCst);
2671 if self.fail_parts {
2672 return Box::pin(async {
2673 Err(object_store::Error::Generic {
2674 store: "ObservedMultipartStore",
2675 source: "injected multipart part failure".into(),
2676 })
2677 });
2678 }
2679 self.inner.put_part(data)
2680 }
2681
2682 async fn complete(&mut self) -> OSResult<PutResult> {
2683 self.inner.complete().await
2684 }
2685
2686 async fn abort(&mut self) -> OSResult<()> {
2687 self.observations.abort_count.fetch_add(1, Ordering::SeqCst);
2688 self.inner.abort().await
2689 }
2690 }
2691
2692 #[derive(Debug)]
2693 struct ObservedMultipartStore {
2694 inner: InMemory,
2695 observations: Arc<MultipartObservations>,
2696 fail_parts: bool,
2697 destination_size_adjustment: u64,
2698 }
2699
2700 impl Display for ObservedMultipartStore {
2701 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2702 write!(f, "ObservedMultipartStore")
2703 }
2704 }
2705
2706 #[async_trait]
2707 impl OSObjectStore for ObservedMultipartStore {
2708 async fn put_opts(
2709 &self,
2710 location: &Path,
2711 bytes: PutPayload,
2712 opts: PutOptions,
2713 ) -> OSResult<PutResult> {
2714 self.inner.put_opts(location, bytes, opts).await
2715 }
2716
2717 async fn put_multipart_opts(
2718 &self,
2719 location: &Path,
2720 opts: PutMultipartOptions,
2721 ) -> OSResult<Box<dyn MultipartUpload>> {
2722 let inner = self.inner.put_multipart_opts(location, opts).await?;
2723 Ok(Box::new(ObservedMultipartUpload {
2724 inner,
2725 observations: self.observations.clone(),
2726 fail_parts: self.fail_parts,
2727 }))
2728 }
2729
2730 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
2731 let is_head = options.head;
2732 let mut result = self.inner.get_opts(location, options).await?;
2733 if is_head && location.filename() == Some("destination.bin") {
2734 result.meta.size = result
2735 .meta
2736 .size
2737 .checked_add(self.destination_size_adjustment)
2738 .expect("test destination size should not overflow");
2739 }
2740 Ok(result)
2741 }
2742
2743 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
2744 self.inner.get_ranges(location, ranges).await
2745 }
2746
2747 fn delete_stream(
2748 &self,
2749 locations: BoxStream<'static, OSResult<Path>>,
2750 ) -> BoxStream<'static, OSResult<Path>> {
2751 self.inner.delete_stream(locations)
2752 }
2753
2754 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
2755 self.inner.list(prefix)
2756 }
2757
2758 fn list_with_offset(
2759 &self,
2760 prefix: Option<&Path>,
2761 offset: &Path,
2762 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
2763 self.inner.list_with_offset(prefix, offset)
2764 }
2765
2766 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
2767 self.inner.list_with_delimiter(prefix).await
2768 }
2769
2770 async fn copy_opts(&self, from: &Path, to: &Path, opts: CopyOptions) -> OSResult<()> {
2771 self.observations
2772 .native_copy_count
2773 .fetch_add(1, Ordering::SeqCst);
2774 self.inner.copy_opts(from, to, opts).await
2775 }
2776 }
2777
2778 #[async_trait]
2779 impl OSObjectStore for CopyFailingStore {
2780 async fn put_opts(
2781 &self,
2782 location: &Path,
2783 bytes: PutPayload,
2784 opts: PutOptions,
2785 ) -> OSResult<PutResult> {
2786 self.inner.put_opts(location, bytes, opts).await
2787 }
2788 async fn put_multipart_opts(
2789 &self,
2790 location: &Path,
2791 opts: PutMultipartOptions,
2792 ) -> OSResult<Box<dyn MultipartUpload>> {
2793 self.inner.put_multipart_opts(location, opts).await
2794 }
2795 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
2796 self.inner.get_opts(location, options).await
2797 }
2798 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
2799 self.inner.get_ranges(location, ranges).await
2800 }
2801 fn delete_stream(
2802 &self,
2803 locations: BoxStream<'static, OSResult<Path>>,
2804 ) -> BoxStream<'static, OSResult<Path>> {
2805 self.inner.delete_stream(locations)
2806 }
2807 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
2808 self.inner.list(prefix)
2809 }
2810 fn list_with_offset(
2811 &self,
2812 prefix: Option<&Path>,
2813 offset: &Path,
2814 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
2815 self.inner.list_with_offset(prefix, offset)
2816 }
2817 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
2818 self.inner.list_with_delimiter(prefix).await
2819 }
2820 async fn copy_opts(&self, _from: &Path, _to: &Path, _opts: CopyOptions) -> OSResult<()> {
2821 Err(object_store::Error::Generic {
2822 store: "CopyFailingStore",
2823 source: "single-shot copy disabled in test".into(),
2824 })
2825 }
2826 }
2827
2828 #[tokio::test]
2829 async fn test_copy_streams_objects_larger_than_threshold() {
2830 let mut store = ObjectStore::memory();
2836 store.inner = Arc::new(CopyFailingStore {
2837 inner: InMemory::new(),
2838 });
2839
2840 let from = Path::from("source.bin");
2841 let contents = b"streaming multipart copy payload well past the tiny threshold";
2842 store.put(&from, contents).await.unwrap();
2843
2844 let streamed = Path::from("streamed.bin");
2847 store.copy_impl(&from, &streamed, true, 8).await.unwrap();
2848 let copied = store.read_one_all(&streamed).await.unwrap();
2849 assert_eq!(copied.as_ref(), contents.as_slice());
2850
2851 let native = Path::from("native.bin");
2855 assert!(
2856 store
2857 .copy_impl(&from, &native, true, u64::MAX)
2858 .await
2859 .is_err()
2860 );
2861 }
2862
2863 #[tokio::test]
2864 async fn test_copy_via_stream_never_uses_native_copy() {
2865 let mut store = ObjectStore::memory();
2866 store.inner = Arc::new(CopyFailingStore {
2867 inner: InMemory::new(),
2868 });
2869
2870 let source = Path::from("source.bin");
2871 let destination = Path::from("destination.bin");
2872 let contents = b"stream raw bytes instead of issuing native copy";
2873 store.put(&source, contents).await.unwrap();
2874
2875 let result = store
2876 .copy_via_stream(&source, &store, &destination)
2877 .await
2878 .unwrap();
2879
2880 assert_eq!(result.size, contents.len());
2881 assert_eq!(
2882 store.read_one_all(&destination).await.unwrap().as_ref(),
2883 contents
2884 );
2885 }
2886
2887 #[tokio::test]
2888 async fn test_bulk_copy_streams_when_server_side_copy_is_disabled() {
2889 let observations = Arc::new(MultipartObservations::default());
2890 let mut store = ObjectStore::memory();
2891 store.inner = Arc::new(ObservedMultipartStore {
2892 inner: InMemory::new(),
2893 observations: observations.clone(),
2894 fail_parts: false,
2895 destination_size_adjustment: 0,
2896 });
2897
2898 let source = Path::from("source.bin");
2899 let destination = Path::from("destination.bin");
2900 let contents = b"stream by default";
2901 store.put(&source, contents).await.unwrap();
2902
2903 let result = store
2904 .copy_bulk_with_server_side_copy(&source, &store, &destination, false)
2905 .await
2906 .unwrap();
2907
2908 assert_eq!(result.size, contents.len());
2909 assert_eq!(observations.native_copy_count.load(Ordering::SeqCst), 0);
2910 assert_eq!(
2911 store.read_one_all(&destination).await.unwrap().as_ref(),
2912 contents
2913 );
2914 }
2915
2916 #[test]
2917 #[serial(server_side_copy_env)]
2918 fn test_server_side_copy_environment_policy() {
2919 let previous_value = std::env::var_os(SERVER_SIDE_COPY_ENABLED_ENV);
2920 let mut store = ObjectStore::memory();
2921 store.scheme = "test-cloud".to_string();
2922 let destination_store = store.clone();
2923
2924 unsafe { std::env::remove_var(SERVER_SIDE_COPY_ENABLED_ENV) };
2927 assert!(!store.uses_server_side_copy(&destination_store));
2928
2929 unsafe { std::env::set_var(SERVER_SIDE_COPY_ENABLED_ENV, "true") };
2931 assert!(store.uses_server_side_copy(&destination_store));
2932
2933 unsafe {
2935 match previous_value {
2936 Some(value) => std::env::set_var(SERVER_SIDE_COPY_ENABLED_ENV, value),
2937 None => std::env::remove_var(SERVER_SIDE_COPY_ENABLED_ENV),
2938 }
2939 }
2940 }
2941
2942 #[tokio::test]
2943 async fn test_bulk_copy_uses_server_side_copy_when_enabled_for_same_store() {
2944 let observations = Arc::new(MultipartObservations::default());
2945 let mut source_store = ObjectStore::memory();
2946 source_store.scheme = "test-cloud".to_string();
2947 source_store.inner = Arc::new(ObservedMultipartStore {
2948 inner: InMemory::new(),
2949 observations: observations.clone(),
2950 fail_parts: false,
2951 destination_size_adjustment: 0,
2952 });
2953 let destination_store = source_store.clone();
2954
2955 let source = Path::from("source.bin");
2956 let destination = Path::from("destination.bin");
2957 let contents = b"use native copy when explicitly enabled";
2958 source_store.put(&source, contents).await.unwrap();
2959
2960 let result = source_store
2961 .copy_bulk_with_server_side_copy(&source, &destination_store, &destination, true)
2962 .await
2963 .unwrap();
2964
2965 assert_eq!(result.size, contents.len());
2966 assert_eq!(observations.native_copy_count.load(Ordering::SeqCst), 1);
2967 assert_eq!(
2968 destination_store
2969 .read_one_all(&destination)
2970 .await
2971 .unwrap()
2972 .as_ref(),
2973 contents
2974 );
2975 }
2976
2977 #[tokio::test]
2978 async fn test_bulk_copy_streams_for_distinct_clients_with_same_prefix() {
2979 let shared_inner = InMemory::new();
2980 let source_observations = Arc::new(MultipartObservations::default());
2981 let mut source_store = ObjectStore::memory();
2982 source_store.scheme = "test-cloud".to_string();
2983 source_store.store_prefix = "test-cloud$bucket".to_string();
2984 source_store.inner = Arc::new(ObservedMultipartStore {
2985 inner: shared_inner.clone(),
2986 observations: source_observations.clone(),
2987 fail_parts: false,
2988 destination_size_adjustment: 0,
2989 });
2990 let destination_observations = Arc::new(MultipartObservations::default());
2991 let mut destination_store = ObjectStore::memory();
2992 destination_store.scheme = "test-cloud".to_string();
2993 destination_store.store_prefix = "test-cloud$bucket".to_string();
2994 destination_store.inner = Arc::new(ObservedMultipartStore {
2995 inner: shared_inner,
2996 observations: destination_observations.clone(),
2997 fail_parts: false,
2998 destination_size_adjustment: 0,
2999 });
3000
3001 let source = Path::from("source.bin");
3002 let destination = Path::from("destination.bin");
3003 let contents = b"use native copy when explicitly enabled";
3004 source_store.put(&source, contents).await.unwrap();
3005
3006 let result = source_store
3007 .copy_bulk_with_server_side_copy(&source, &destination_store, &destination, true)
3008 .await
3009 .unwrap();
3010
3011 assert_eq!(result.size, contents.len());
3012 assert_eq!(
3013 source_observations.native_copy_count.load(Ordering::SeqCst),
3014 0
3015 );
3016 assert_eq!(
3017 destination_observations
3018 .native_copy_count
3019 .load(Ordering::SeqCst),
3020 0
3021 );
3022 assert_eq!(
3023 destination_store
3024 .read_one_all(&destination)
3025 .await
3026 .unwrap()
3027 .as_ref(),
3028 contents
3029 );
3030 }
3031
3032 #[tokio::test]
3033 async fn test_bulk_copy_rejects_server_side_destination_size_mismatch() {
3034 let observations = Arc::new(MultipartObservations::default());
3035 let mut source_store = ObjectStore::memory();
3036 source_store.scheme = "test-cloud".to_string();
3037 source_store.inner = Arc::new(ObservedMultipartStore {
3038 inner: InMemory::new(),
3039 observations: observations.clone(),
3040 fail_parts: false,
3041 destination_size_adjustment: 1,
3042 });
3043 let destination_store = source_store.clone();
3044
3045 let source = Path::from("source.bin");
3046 let destination = Path::from("destination.bin");
3047 source_store
3048 .put(&source, b"validate native copy")
3049 .await
3050 .unwrap();
3051
3052 let error = source_store
3053 .copy_bulk_with_server_side_copy(&source, &destination_store, &destination, true)
3054 .await
3055 .unwrap_err();
3056
3057 assert!(
3058 error.to_string().contains("destination size mismatch"),
3059 "expected validation failure, got: {error}"
3060 );
3061 assert_eq!(observations.native_copy_count.load(Ordering::SeqCst), 1);
3062 }
3063
3064 #[tokio::test]
3065 async fn test_bulk_copy_streams_across_stores_when_server_side_copy_is_enabled() {
3066 let source_store = ObjectStore::memory();
3067 let observations = Arc::new(MultipartObservations::default());
3068 let mut destination_store = ObjectStore::memory();
3069 destination_store.inner = Arc::new(ObservedMultipartStore {
3070 inner: InMemory::new(),
3071 observations: observations.clone(),
3072 fail_parts: false,
3073 destination_size_adjustment: 0,
3074 });
3075
3076 let source = Path::from("source.bin");
3077 let destination = Path::from("destination.bin");
3078 let contents = b"cross-store copies must stream";
3079 source_store.put(&source, contents).await.unwrap();
3080
3081 let result = source_store
3082 .copy_bulk_with_server_side_copy(&source, &destination_store, &destination, true)
3083 .await
3084 .unwrap();
3085
3086 assert_eq!(result.size, contents.len());
3087 assert_eq!(observations.native_copy_count.load(Ordering::SeqCst), 0);
3088 assert_eq!(
3089 destination_store
3090 .read_one_all(&destination)
3091 .await
3092 .unwrap()
3093 .as_ref(),
3094 contents
3095 );
3096 }
3097
3098 #[tokio::test]
3099 async fn test_copy_via_stream_preserves_local_not_found() {
3100 let directory = TempStdDir::default();
3101 let (store, base_path) = ObjectStore::from_uri(directory.to_str().unwrap())
3102 .await
3103 .unwrap();
3104 let source = base_path.clone().join("missing.bin");
3105 let destination = base_path.join("destination.bin");
3106
3107 let error = store
3108 .copy_via_stream(&source, &store, &destination)
3109 .await
3110 .unwrap_err();
3111
3112 assert!(
3113 error.is_not_found(),
3114 "expected not-found error, got: {error}"
3115 );
3116 }
3117
3118 #[tokio::test]
3119 async fn test_copy_via_stream_uses_multiple_parts() {
3120 let mut source_store = ObjectStore::memory();
3121 source_store.max_iop_size = 1024 * 1024;
3122 let observations = Arc::new(MultipartObservations::default());
3123 let mut destination_store = ObjectStore::memory();
3124 destination_store.inner = Arc::new(ObservedMultipartStore {
3125 inner: InMemory::new(),
3126 observations: observations.clone(),
3127 fail_parts: false,
3128 destination_size_adjustment: 0,
3129 });
3130
3131 let source = Path::from("source.bin");
3132 let destination = Path::from("destination.bin");
3133 let contents = vec![42; crate::object_writer::initial_upload_size() * 2 + 1];
3134 source_store.put(&source, &contents).await.unwrap();
3135
3136 let result = source_store
3137 .copy_via_stream(&source, &destination_store, &destination)
3138 .await
3139 .unwrap();
3140
3141 assert_eq!(result.size, contents.len());
3142 assert!(
3143 observations.part_count.load(Ordering::SeqCst) >= 2,
3144 "stream copy should split a large destination into multiple upload parts"
3145 );
3146 assert_eq!(
3147 destination_store
3148 .read_one_all(&destination)
3149 .await
3150 .unwrap()
3151 .as_ref(),
3152 contents.as_slice()
3153 );
3154 }
3155
3156 #[tokio::test]
3157 async fn test_copy_via_stream_aborts_failed_upload_and_retains_source() {
3158 let source_store = ObjectStore::memory();
3159 let observations = Arc::new(MultipartObservations::default());
3160 let mut destination_store = ObjectStore::memory();
3161 destination_store.inner = Arc::new(ObservedMultipartStore {
3162 inner: InMemory::new(),
3163 observations: observations.clone(),
3164 fail_parts: true,
3165 destination_size_adjustment: 0,
3166 });
3167
3168 let source = Path::from("source.bin");
3169 let destination = Path::from("destination.bin");
3170 let contents = vec![7; crate::object_writer::initial_upload_size() * 2];
3171 source_store.put(&source, &contents).await.unwrap();
3172
3173 let error = source_store
3174 .copy_via_stream(&source, &destination_store, &destination)
3175 .await
3176 .unwrap_err();
3177 let error_message = error.to_string();
3178 assert!(
3179 (error_message.contains("destination write")
3180 || error_message.contains("destination completion"))
3181 && error_message.contains("injected multipart part failure"),
3182 "expected upload-stage context and the underlying error, got: {error}"
3183 );
3184
3185 tokio::time::timeout(Duration::from_secs(1), async {
3186 loop {
3187 if observations.abort_count.load(Ordering::SeqCst) > 0 {
3188 break;
3189 }
3190 tokio::task::yield_now().await;
3191 }
3192 })
3193 .await
3194 .expect("multipart abort should complete");
3195 assert_eq!(observations.abort_count.load(Ordering::SeqCst), 1);
3196 assert_eq!(
3197 source_store.read_one_all(&source).await.unwrap().as_ref(),
3198 contents.as_slice()
3199 );
3200 assert!(!destination_store.exists(&destination).await.unwrap());
3201 }
3202
3203 #[tokio::test]
3204 async fn test_copy_via_stream_rejects_destination_size_mismatch() {
3205 let source_store = ObjectStore::memory();
3206 let mut destination_store = ObjectStore::memory();
3207 destination_store.inner = Arc::new(ObservedMultipartStore {
3208 inner: InMemory::new(),
3209 observations: Arc::new(MultipartObservations::default()),
3210 fail_parts: false,
3211 destination_size_adjustment: 1,
3212 });
3213
3214 let source = Path::from("source.bin");
3215 let destination = Path::from("destination.bin");
3216 let contents = b"validate the destination after completion";
3217 source_store.put(&source, contents).await.unwrap();
3218
3219 let error = source_store
3220 .copy_via_stream(&source, &destination_store, &destination)
3221 .await
3222 .unwrap_err();
3223
3224 assert!(
3225 error.to_string().contains("destination size mismatch"),
3226 "expected validation failure, got: {error}"
3227 );
3228 }
3229
3230 #[test]
3231 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
3232 fn test_client_options_extracts_headers() {
3233 let opts = StorageOptions(HashMap::from([
3234 ("headers.x-custom-foo".to_string(), "bar".to_string()),
3235 ("headers.x-ms-version".to_string(), "2023-11-03".to_string()),
3236 ("region".to_string(), "us-west-2".to_string()),
3237 ]));
3238 let client_options = opts.client_options().unwrap();
3239
3240 let opts_no_headers = StorageOptions(HashMap::from([(
3243 "region".to_string(),
3244 "us-west-2".to_string(),
3245 )]));
3246 opts_no_headers.client_options().unwrap();
3247
3248 #[cfg(feature = "gcp")]
3252 {
3253 use object_store::gcp::GoogleCloudStorageBuilder;
3254 let _builder = GoogleCloudStorageBuilder::new()
3255 .with_client_options(client_options)
3256 .with_url("gs://test-bucket");
3257 }
3258 }
3259
3260 #[test]
3261 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
3262 fn test_client_options_rejects_invalid_header_name() {
3263 let opts = StorageOptions(HashMap::from([(
3264 "headers.bad header".to_string(),
3265 "value".to_string(),
3266 )]));
3267 let err = opts.client_options().unwrap_err();
3268 assert!(err.to_string().contains("invalid header name"));
3269 }
3270
3271 #[test]
3272 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
3273 fn test_client_options_rejects_invalid_header_value() {
3274 let opts = StorageOptions(HashMap::from([(
3275 "headers.x-good-name".to_string(),
3276 "bad\x01value".to_string(),
3277 )]));
3278 let err = opts.client_options().unwrap_err();
3279 assert!(err.to_string().contains("invalid header value"));
3280 }
3281
3282 #[test]
3283 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
3284 fn test_client_options_empty_when_no_header_keys() {
3285 let opts = StorageOptions(HashMap::from([
3286 ("region".to_string(), "us-east-1".to_string()),
3287 ("access_key_id".to_string(), "AKID".to_string()),
3288 ]));
3289 opts.client_options().unwrap();
3290 }
3291}