1use std::borrow::Cow;
7use std::collections::HashMap;
8use std::ops::Range;
9use std::pin::Pin;
10use std::str::FromStr;
11use std::sync::Arc;
12use std::time::Duration;
13
14use async_trait::async_trait;
15use bytes::Bytes;
16use chrono::{DateTime, Utc};
17use futures::{FutureExt, Stream};
18use futures::{StreamExt, TryStreamExt, future, stream::BoxStream};
19use lance_core::deepsize::DeepSizeOf;
20use lance_core::error::LanceOptionExt;
21use lance_core::utils::parse::str_is_truthy;
22use list_retry::ListRetryStream;
23use object_store::DynObjectStore;
24use object_store::ObjectStoreExt as OSObjectStoreExt;
25#[cfg(feature = "aws")]
26use object_store::aws::AwsCredentialProvider;
27#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
28use object_store::{ClientOptions, HeaderMap, HeaderValue};
29use object_store::{ListResult, ObjectMeta, ObjectStore as OSObjectStore, path::Path};
30use providers::local::FileStoreProvider;
31use providers::memory::MemoryStoreProvider;
32use tokio::io::AsyncWriteExt;
33use url::Url;
34
35use super::local::LocalObjectReader;
36#[cfg(target_os = "linux")]
37use crate::uring::{UringCurrentThreadReader, UringReader};
38#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
39pub(crate) mod dynamic_credentials;
40#[cfg(any(feature = "oss", feature = "huggingface", feature = "tos"))]
41pub(crate) mod dynamic_opendal;
42mod list_retry;
43#[cfg(feature = "metrics")]
44pub mod metrics;
45pub mod providers;
46pub mod storage_options;
47#[cfg(test)]
48pub(crate) mod test_utils;
49pub mod throttle;
50mod tracing;
51use crate::object_reader::SmallReader;
52use crate::object_writer::{LocalWriter, WriteResult};
53use crate::traits::{WriteExt, Writer};
54use crate::utils::tracking_store::{IOTracker, IoStats};
55use crate::{object_reader::CloudObjectReader, object_writer::ObjectWriter, traits::Reader};
56use lance_core::{Error, Result};
57
58pub const DEFAULT_LOCAL_IO_PARALLELISM: usize = 8;
63pub const DEFAULT_CLOUD_IO_PARALLELISM: usize = 64;
65
66const DEFAULT_LOCAL_BLOCK_SIZE: usize = 4 * 1024; #[cfg(any(
68 feature = "aws",
69 feature = "gcp",
70 feature = "azure",
71 feature = "oss",
72 feature = "tencent",
73 feature = "huggingface",
74 feature = "tos",
75 feature = "goosefs",
76))]
77const DEFAULT_CLOUD_BLOCK_SIZE: usize = 64 * 1024; pub static DEFAULT_MAX_IOP_SIZE: std::sync::LazyLock<u64> = std::sync::LazyLock::new(|| {
80 std::env::var("LANCE_MAX_IOP_SIZE")
81 .map(|val| val.parse().unwrap())
82 .unwrap_or(16 * 1024 * 1024)
83});
84
85pub const DEFAULT_DOWNLOAD_RETRY_COUNT: usize = 3;
86
87pub use providers::{ObjectStoreProvider, ObjectStoreRegistry};
88pub use storage_options::{
89 BASE_SCOPED_OPTION_PREFIX, BaseScopedStorageOptionsProvider, EXPIRES_AT_MILLIS_KEY,
90 LanceNamespaceStorageOptionsProvider, REFRESH_OFFSET_MILLIS_KEY, StorageOptionsAccessor,
91 StorageOptionsProvider, has_base_scoped_options, parse_base_scoped_key,
92 resolve_base_scoped_options,
93};
94
95#[async_trait]
96pub trait ObjectStoreExt {
97 async fn exists(&self, path: &Path) -> Result<bool>;
99
100 fn read_dir_all<'a, 'b>(
104 &'a self,
105 dir_path: impl Into<&'b Path> + Send,
106 unmodified_since: Option<DateTime<Utc>>,
107 ) -> BoxStream<'a, Result<ObjectMeta>>;
108}
109
110#[async_trait]
111impl<O: OSObjectStore + ?Sized> ObjectStoreExt for O {
112 fn read_dir_all<'a, 'b>(
113 &'a self,
114 dir_path: impl Into<&'b Path> + Send,
115 unmodified_since: Option<DateTime<Utc>>,
116 ) -> BoxStream<'a, Result<ObjectMeta>> {
117 let output = self.list(Some(dir_path.into())).map_err(|e| e.into());
118 if let Some(unmodified_since_val) = unmodified_since {
119 output
120 .try_filter(move |file| future::ready(file.last_modified <= unmodified_since_val))
121 .boxed()
122 } else {
123 output.boxed()
124 }
125 }
126
127 async fn exists(&self, path: &Path) -> Result<bool> {
128 match self.head(path).await {
129 Ok(_) => Ok(true),
130 Err(object_store::Error::NotFound { path: _, source: _ }) => Ok(false),
131 Err(e) => Err(e.into()),
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct ObjectStore {
139 pub inner: Arc<dyn OSObjectStore>,
141 scheme: String,
142 block_size: usize,
143 max_iop_size: u64,
144 pub use_constant_size_upload_parts: bool,
147 pub list_is_lexically_ordered: bool,
150 io_parallelism: usize,
151 download_retry_count: usize,
153 io_tracker: IOTracker,
155 pub store_prefix: String,
159}
160
161impl DeepSizeOf for ObjectStore {
162 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
163 self.scheme.deep_size_of_children(context) + self.block_size.deep_size_of_children(context)
168 }
169}
170
171impl std::fmt::Display for ObjectStore {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 write!(f, "ObjectStore({})", self.scheme)
174 }
175}
176
177pub trait WrappingObjectStore: std::fmt::Debug + Send + Sync {
178 fn wrap(&self, store_prefix: &str, original: Arc<dyn OSObjectStore>) -> Arc<dyn OSObjectStore>;
183}
184
185#[derive(Debug, Clone)]
186pub struct ChainedWrappingObjectStore {
187 wrappers: Vec<Arc<dyn WrappingObjectStore>>,
188}
189
190impl ChainedWrappingObjectStore {
191 pub fn new(wrappers: Vec<Arc<dyn WrappingObjectStore>>) -> Self {
192 Self { wrappers }
193 }
194
195 pub fn add_wrapper(&mut self, wrapper: Arc<dyn WrappingObjectStore>) {
196 self.wrappers.push(wrapper);
197 }
198}
199
200impl WrappingObjectStore for ChainedWrappingObjectStore {
201 fn wrap(&self, store_prefix: &str, original: Arc<dyn OSObjectStore>) -> Arc<dyn OSObjectStore> {
202 self.wrappers
203 .iter()
204 .fold(original, |acc, wrapper| wrapper.wrap(store_prefix, acc))
205 }
206}
207
208#[derive(Debug, Clone)]
211pub struct ObjectStoreParams {
212 pub block_size: Option<usize>,
213 #[deprecated(note = "Implement an ObjectStoreProvider instead")]
214 pub object_store: Option<(Arc<DynObjectStore>, Url)>,
215 pub s3_credentials_refresh_offset: Duration,
218 #[cfg(feature = "aws")]
219 pub aws_credentials: Option<AwsCredentialProvider>,
220 pub object_store_wrapper: Option<Arc<dyn WrappingObjectStore>>,
221 pub storage_options_accessor: Option<Arc<StorageOptionsAccessor>>,
227 pub use_constant_size_upload_parts: bool,
232 pub list_is_lexically_ordered: Option<bool>,
233}
234
235impl Default for ObjectStoreParams {
236 fn default() -> Self {
237 #[allow(deprecated)]
238 Self {
239 object_store: None,
240 block_size: None,
241 s3_credentials_refresh_offset: Duration::from_secs(60),
242 #[cfg(feature = "aws")]
243 aws_credentials: None,
244 object_store_wrapper: None,
245 storage_options_accessor: None,
246 use_constant_size_upload_parts: false,
247 list_is_lexically_ordered: None,
248 }
249 }
250}
251
252impl ObjectStoreParams {
253 pub fn get_accessor(&self) -> Option<Arc<StorageOptionsAccessor>> {
255 self.storage_options_accessor.clone()
256 }
257
258 pub fn storage_options(&self) -> Option<&HashMap<String, String>> {
262 self.storage_options_accessor
263 .as_ref()
264 .and_then(|a| a.initial_storage_options())
265 }
266
267 pub fn scoped_to_base(&self, base_id: Option<u32>) -> Cow<'_, Self> {
274 let Some(accessor) = &self.storage_options_accessor else {
275 return Cow::Borrowed(self);
276 };
277 let scoped = accessor.scoped_to_base(base_id);
278 if Arc::ptr_eq(&scoped, accessor) {
279 Cow::Borrowed(self)
280 } else {
281 Cow::Owned(Self {
282 storage_options_accessor: Some(scoped),
283 ..self.clone()
284 })
285 }
286 }
287}
288
289impl std::hash::Hash for ObjectStoreParams {
291 #[allow(deprecated)]
292 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
293 self.block_size.hash(state);
295 if let Some((store, url)) = &self.object_store {
296 Arc::as_ptr(store).hash(state);
297 url.hash(state);
298 }
299 self.s3_credentials_refresh_offset.hash(state);
300 #[cfg(feature = "aws")]
301 if let Some(aws_credentials) = &self.aws_credentials {
302 Arc::as_ptr(aws_credentials).hash(state);
303 }
304 if let Some(wrapper) = &self.object_store_wrapper {
305 Arc::as_ptr(wrapper).hash(state);
306 }
307 if let Some(accessor) = &self.storage_options_accessor {
308 accessor.accessor_id().hash(state);
309 }
310 self.use_constant_size_upload_parts.hash(state);
311 self.list_is_lexically_ordered.hash(state);
312 }
313}
314
315impl Eq for ObjectStoreParams {}
317impl PartialEq for ObjectStoreParams {
318 #[allow(deprecated)]
319 fn eq(&self, other: &Self) -> bool {
320 #[cfg(feature = "aws")]
321 if self.aws_credentials.is_some() != other.aws_credentials.is_some() {
322 return false;
323 }
324
325 self.block_size == other.block_size
328 && self
329 .object_store
330 .as_ref()
331 .map(|(store, url)| (Arc::as_ptr(store), url))
332 == other
333 .object_store
334 .as_ref()
335 .map(|(store, url)| (Arc::as_ptr(store), url))
336 && self.s3_credentials_refresh_offset == other.s3_credentials_refresh_offset
337 && self.object_store_wrapper.as_ref().map(Arc::as_ptr)
338 == other.object_store_wrapper.as_ref().map(Arc::as_ptr)
339 && self
340 .storage_options_accessor
341 .as_ref()
342 .map(|a| a.accessor_id())
343 == other
344 .storage_options_accessor
345 .as_ref()
346 .map(|a| a.accessor_id())
347 && self.use_constant_size_upload_parts == other.use_constant_size_upload_parts
348 && self.list_is_lexically_ordered == other.list_is_lexically_ordered
349 }
350}
351
352pub fn uri_to_url(uri: &str) -> Result<Url> {
373 match Url::parse(uri) {
374 Ok(url) if url.scheme().len() == 1 && cfg!(windows) => {
375 local_path_to_url(uri)
377 }
378 Ok(url) => Ok(url),
379 Err(_) => local_path_to_url(uri),
380 }
381}
382
383fn expand_path(str_path: impl AsRef<str>) -> Result<std::path::PathBuf> {
384 let str_path = str_path.as_ref();
385 let expanded = expand_tilde_path(str_path).unwrap_or_else(|| str_path.into());
386
387 let mut expanded_path = path_abs::PathAbs::new(expanded)
388 .unwrap()
389 .as_path()
390 .to_path_buf();
391 if let Some(s) = expanded_path.as_path().to_str()
393 && s.is_empty()
394 {
395 expanded_path = std::env::current_dir()?;
396 }
397
398 Ok(expanded_path)
399}
400
401fn expand_tilde_path(path: &str) -> Option<std::path::PathBuf> {
402 let home_dir = std::env::home_dir()?;
403 if path == "~" {
404 return Some(home_dir);
405 }
406 if let Some(stripped) = path.strip_prefix("~/") {
407 return Some(home_dir.join(stripped));
408 }
409 #[cfg(windows)]
410 if let Some(stripped) = path.strip_prefix("~\\") {
411 return Some(home_dir.join(stripped));
412 }
413
414 None
415}
416
417fn local_path_to_url(str_path: &str) -> Result<Url> {
418 let expanded_path = expand_path(str_path)?;
419
420 Url::from_directory_path(expanded_path).map_err(|_| {
421 Error::invalid_input_source(format!("Invalid table location: '{}'", str_path).into())
422 })
423}
424
425#[cfg(feature = "huggingface")]
426fn parse_hf_repo_id(url: &Url) -> Result<String> {
427 let mut segments: Vec<String> = Vec::new();
429 if let Some(host) = url.host_str() {
430 segments.push(host.to_string());
431 }
432 segments.extend(
433 url.path()
434 .trim_start_matches('/')
435 .split('/')
436 .map(|s| s.to_string()),
437 );
438
439 if segments.len() < 2 {
440 return Err(Error::invalid_input(
441 "Huggingface URL must contain at least owner and repo",
442 ));
443 }
444
445 let repo_type_candidates = ["models", "datasets", "spaces"];
446 let (owner, repo_with_rev) = if repo_type_candidates.contains(&segments[0].as_str()) {
447 if segments.len() < 3 {
448 return Err(Error::invalid_input(
449 "Huggingface URL missing owner/repo after repo type",
450 ));
451 }
452 (segments[1].as_str(), segments[2].as_str())
453 } else {
454 (segments[0].as_str(), segments[1].as_str())
455 };
456
457 let repo = repo_with_rev
458 .split_once('@')
459 .map(|(r, _)| r)
460 .unwrap_or(repo_with_rev);
461 Ok(format!("{owner}/{repo}"))
462}
463
464impl ObjectStore {
465 pub async fn from_uri(uri: &str) -> Result<(Arc<Self>, Path)> {
473 let registry = Arc::new(ObjectStoreRegistry::default());
474
475 Self::from_uri_and_params(registry, uri, &ObjectStoreParams::default()).await
476 }
477
478 pub async fn from_uri_and_params(
482 registry: Arc<ObjectStoreRegistry>,
483 uri: &str,
484 params: &ObjectStoreParams,
485 ) -> Result<(Arc<Self>, Path)> {
486 #[allow(deprecated)]
487 if let Some((store, path)) = params.object_store.as_ref() {
488 let mut inner = store.clone();
489 let store_prefix =
490 registry.calculate_object_store_prefix(uri, params.storage_options())?;
491 if let Some(wrapper) = params.object_store_wrapper.as_ref() {
492 inner = wrapper.wrap(&store_prefix, inner);
493 }
494
495 let io_tracker = IOTracker::default();
497 let tracked_store = io_tracker.wrap("", inner);
498
499 let store = Self {
500 inner: tracked_store,
501 scheme: path.scheme().to_string(),
502 block_size: params.block_size.unwrap_or(64 * 1024),
503 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
504 use_constant_size_upload_parts: params.use_constant_size_upload_parts,
505 list_is_lexically_ordered: params.list_is_lexically_ordered.unwrap_or_default(),
506 io_parallelism: DEFAULT_CLOUD_IO_PARALLELISM,
507 download_retry_count: DEFAULT_DOWNLOAD_RETRY_COUNT,
508 io_tracker,
509 store_prefix,
510 };
511 let path = Path::parse(path.path())?;
512 return Ok((Arc::new(store), path));
513 }
514 let url = uri_to_url(uri)?;
515
516 let store = registry.get_store(url.clone(), params).await?;
517 let provider = registry.get_provider(url.scheme()).expect_ok()?;
519 let path = provider.extract_path(&url)?;
520
521 Ok((store, path))
522 }
523
524 pub fn extract_path_from_uri(registry: Arc<ObjectStoreRegistry>, uri: &str) -> Result<Path> {
538 let url = uri_to_url(uri)?;
539 let provider = registry
540 .get_provider(url.scheme())
541 .ok_or_else(|| Error::invalid_input(format!("Unknown scheme: {}", url.scheme())))?;
542 provider.extract_path(&url)
543 }
544
545 #[deprecated(note = "Use `from_uri` instead")]
546 pub fn from_path(str_path: &str) -> Result<(Arc<Self>, Path)> {
547 Self::from_uri_and_params(
548 Arc::new(ObjectStoreRegistry::default()),
549 str_path,
550 &Default::default(),
551 )
552 .now_or_never()
553 .unwrap()
554 }
555
556 pub fn local() -> Self {
558 let provider = FileStoreProvider;
559 provider
560 .new_store(Url::parse("file:///").unwrap(), &Default::default())
561 .now_or_never()
562 .unwrap()
563 .unwrap()
564 }
565
566 pub fn memory() -> Self {
568 let provider = MemoryStoreProvider;
569 provider
570 .new_store(Url::parse("memory:///").unwrap(), &Default::default())
571 .now_or_never()
572 .unwrap()
573 .unwrap()
574 }
575
576 pub fn is_local(&self) -> bool {
578 self.scheme == "file" || self.scheme == "file+uring"
579 }
580
581 pub fn is_cloud(&self) -> bool {
582 if self.is_local() || self.scheme == "memory" || self.scheme == "shared-memory" {
583 return false;
584 }
585 true
586 }
587
588 pub fn prefers_lite_scheduler(&self) -> bool {
593 self.scheme == "file+uring"
594 }
595
596 pub fn scheme(&self) -> &str {
597 &self.scheme
598 }
599
600 pub fn block_size(&self) -> usize {
601 self.block_size
602 }
603
604 pub fn max_iop_size(&self) -> u64 {
605 self.max_iop_size
606 }
607
608 pub fn io_parallelism(&self) -> usize {
615 std::env::var("LANCE_IO_THREADS")
616 .map(|val| val.parse::<usize>().unwrap())
617 .unwrap_or(self.io_parallelism)
618 .max(1)
619 }
620
621 pub fn io_tracker(&self) -> &IOTracker {
626 &self.io_tracker
627 }
628
629 pub fn io_stats_snapshot(&self) -> IoStats {
634 self.io_tracker.stats()
635 }
636
637 pub fn io_stats_incremental(&self) -> IoStats {
643 self.io_tracker.incremental_stats()
644 }
645
646 pub async fn open(&self, path: &Path) -> Result<Box<dyn Reader>> {
651 match self.scheme.as_str() {
652 "file" => {
653 LocalObjectReader::open_with_tracker(
654 path,
655 self.block_size,
656 None,
657 Arc::new(self.io_tracker.clone()),
658 )
659 .await
660 }
661 #[cfg(target_os = "linux")]
662 "file+uring" => {
663 let use_current_thread = std::env::var("LANCE_URING_CURRENT_THREAD")
665 .map(|v| str_is_truthy(&v))
666 .unwrap_or(false);
667
668 if use_current_thread {
669 UringCurrentThreadReader::open(
670 path,
671 self.block_size,
672 None,
673 Arc::new(self.io_tracker.clone()),
674 )
675 .await
676 } else {
677 UringReader::open(
678 path,
679 self.block_size,
680 None,
681 Arc::new(self.io_tracker.clone()),
682 )
683 .await
684 }
685 }
686 _ => Ok(Box::new(CloudObjectReader::new(
687 self.inner.clone(),
688 path.clone(),
689 self.block_size,
690 None,
691 self.download_retry_count,
692 )?)),
693 }
694 }
695
696 pub async fn open_with_size(&self, path: &Path, known_size: usize) -> Result<Box<dyn Reader>> {
702 if known_size <= self.block_size {
705 return Ok(Box::new(SmallReader::new(
706 self.inner.clone(),
707 path.clone(),
708 self.download_retry_count,
709 known_size,
710 )));
711 }
712
713 match self.scheme.as_str() {
714 "file" => {
715 LocalObjectReader::open_with_tracker(
716 path,
717 self.block_size,
718 Some(known_size),
719 Arc::new(self.io_tracker.clone()),
720 )
721 .await
722 }
723 #[cfg(target_os = "linux")]
724 "file+uring" => {
725 let use_current_thread = std::env::var("LANCE_URING_CURRENT_THREAD")
727 .map(|v| str_is_truthy(&v))
728 .unwrap_or(false);
729
730 if use_current_thread {
731 UringCurrentThreadReader::open(
732 path,
733 self.block_size,
734 Some(known_size),
735 Arc::new(self.io_tracker.clone()),
736 )
737 .await
738 } else {
739 UringReader::open(
740 path,
741 self.block_size,
742 Some(known_size),
743 Arc::new(self.io_tracker.clone()),
744 )
745 .await
746 }
747 }
748 _ => Ok(Box::new(CloudObjectReader::new(
749 self.inner.clone(),
750 path.clone(),
751 self.block_size,
752 Some(known_size),
753 self.download_retry_count,
754 )?)),
755 }
756 }
757
758 pub async fn create_local_writer(path: &std::path::Path) -> Result<ObjectWriter> {
760 let object_store = Self::local();
761 let absolute_path = expand_path(path.to_string_lossy())?;
762 let os_path = Path::from_absolute_path(absolute_path)?;
763 ObjectWriter::new(&object_store, &os_path).await
764 }
765
766 pub async fn open_local(path: &std::path::Path) -> Result<Box<dyn Reader>> {
768 let object_store = Self::local();
769 let absolute_path = expand_path(path.to_string_lossy())?;
770 let os_path = Path::from_absolute_path(absolute_path)?;
771 object_store.open(&os_path).await
772 }
773
774 pub async fn create(&self, path: &Path) -> Result<Box<dyn Writer>> {
776 match self.scheme.as_str() {
777 "file" => {
778 let local_path = super::local::to_local_path(path);
779 let local_path = std::path::PathBuf::from(&local_path);
780 if let Some(parent) = local_path.parent() {
781 tokio::fs::create_dir_all(parent).await?;
782 }
783 let parent = local_path
784 .parent()
785 .expect("file path must have parent")
786 .to_owned();
787 let named_temp =
788 tokio::task::spawn_blocking(move || tempfile::NamedTempFile::new_in(parent))
789 .await
790 .map_err(|e| Error::io(format!("spawn_blocking failed: {}", e)))??;
791 let (std_file, temp_path) = named_temp.into_parts();
792 let file = tokio::fs::File::from_std(std_file);
793 Ok(Box::new(LocalWriter::new(
794 file,
795 path.clone(),
796 temp_path,
797 Arc::new(self.io_tracker.clone()),
798 )))
799 }
800 _ => Ok(Box::new(ObjectWriter::new(self, path).await?)),
801 }
802 }
803
804 pub async fn put(&self, path: &Path, content: &[u8]) -> Result<WriteResult> {
806 let mut writer = self.create(path).await?;
807 writer.write_all(content).await?;
808 Writer::shutdown(writer.as_mut()).await
809 }
810
811 pub async fn delete(&self, path: &Path) -> Result<()> {
812 self.inner.delete(path).await?;
813 Ok(())
814 }
815
816 const MAX_SINGLE_COPY_BYTES: u64 = 5 * 1024 * 1024 * 1024; pub async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
821 let multipart_copy_fallback = matches!(self.scheme.as_str(), "s3" | "s3+ddb" | "gs");
827 self.copy_impl(
828 from,
829 to,
830 multipart_copy_fallback,
831 Self::MAX_SINGLE_COPY_BYTES,
832 )
833 .await
834 }
835
836 async fn copy_impl(
842 &self,
843 from: &Path,
844 to: &Path,
845 multipart_copy_fallback: bool,
846 max_single_copy: u64,
847 ) -> Result<()> {
848 if self.is_local() {
849 return super::local::copy_file(from, to);
851 }
852 if multipart_copy_fallback {
853 let reader = self.open(from).await?;
856 if reader.size().await? as u64 > max_single_copy {
857 let mut writer = self.create(to).await?;
858 writer.copy_from_reader(reader.as_ref()).await?;
859 Writer::shutdown(writer.as_mut()).await?;
860 return Ok(());
861 }
862 }
863 Ok(self.inner.copy(from, to).await?)
864 }
865
866 pub async fn read_dir(&self, dir_path: impl Into<Path>) -> Result<Vec<String>> {
868 let path = dir_path.into();
869 let path = Path::parse(&path)?;
870 let output = self.inner.list_with_delimiter(Some(&path)).await?;
871 Ok(output
872 .common_prefixes
873 .iter()
874 .chain(output.objects.iter().map(|o| &o.location))
875 .filter_map(|s| s.filename().map(|f| f.to_string()))
876 .collect())
877 }
878
879 pub async fn list_with_delimiter(&self, prefix: Option<&Path>) -> Result<ListResult> {
886 Ok(self.inner.list_with_delimiter(prefix).await?)
887 }
888
889 pub fn list(
890 &self,
891 path: Option<Path>,
892 ) -> Pin<Box<dyn Stream<Item = Result<ObjectMeta>> + Send>> {
893 Box::pin(ListRetryStream::new(self.inner.clone(), path, 5).map(|m| m.map_err(|e| e.into())))
894 }
895
896 pub fn read_dir_all<'a, 'b>(
900 &'a self,
901 dir_path: impl Into<&'b Path> + Send,
902 unmodified_since: Option<DateTime<Utc>>,
903 ) -> BoxStream<'a, Result<ObjectMeta>> {
904 self.inner.read_dir_all(dir_path, unmodified_since)
905 }
906
907 pub async fn remove_dir_all(&self, dir_path: impl Into<Path>) -> Result<()> {
909 let path = dir_path.into();
910 let path = Path::parse(&path)?;
911
912 if self.is_local() {
913 return super::local::remove_dir_all(&path);
915 }
916 let sub_entries = self
917 .inner
918 .list(Some(&path))
919 .map(|m| m.map(|meta| meta.location))
920 .boxed();
921 self.inner
922 .delete_stream(sub_entries)
923 .try_collect::<Vec<_>>()
924 .await?;
925 if self.scheme == "file-object-store" {
926 return super::local::remove_dir_all(&path);
929 }
930 Ok(())
931 }
932
933 pub fn remove_stream<'a>(
934 &'a self,
935 locations: BoxStream<'a, Result<Path>>,
936 ) -> BoxStream<'a, Result<Path>> {
937 let store = Arc::clone(&self.inner);
938 locations
939 .and_then(move |location| {
940 let store = Arc::clone(&store);
941 async move {
942 store.delete(&location).await?;
943 Ok(location)
944 }
945 })
946 .boxed()
947 }
948
949 pub async fn exists(&self, path: &Path) -> Result<bool> {
951 match self.inner.head(path).await {
952 Ok(_) => Ok(true),
953 Err(object_store::Error::NotFound { path: _, source: _ }) => Ok(false),
954 Err(e) => Err(e.into()),
955 }
956 }
957
958 pub async fn size(&self, path: &Path) -> Result<u64> {
960 Ok(self.inner.head(path).await?.size)
961 }
962
963 pub async fn read_one_all(&self, path: &Path) -> Result<Bytes> {
965 let reader = self.open(path).await?;
966 Ok(reader.get_all().await?)
967 }
968
969 pub async fn read_one_range(&self, path: &Path, range: Range<usize>) -> Result<Bytes> {
974 let reader = self.open(path).await?;
975 Ok(reader.get_range(range).await?)
976 }
977}
978
979#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy)]
981pub enum LanceConfigKey {
982 DownloadRetryCount,
984}
985
986impl FromStr for LanceConfigKey {
987 type Err = Error;
988
989 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
990 match s.to_ascii_lowercase().as_str() {
991 "download_retry_count" => Ok(Self::DownloadRetryCount),
992 _ => Err(Error::invalid_input_source(
993 format!("Invalid LanceConfigKey: {}", s).into(),
994 )),
995 }
996 }
997}
998
999#[derive(Clone, Debug, Default)]
1000pub struct StorageOptions(pub HashMap<String, String>);
1001
1002impl StorageOptions {
1003 pub fn new(options: HashMap<String, String>) -> Self {
1005 let mut options = options;
1006 if let Ok(value) = std::env::var("AZURE_STORAGE_ALLOW_HTTP") {
1007 options.insert("allow_http".into(), value);
1008 }
1009 if let Ok(value) = std::env::var("AZURE_STORAGE_USE_HTTP") {
1010 options.insert("allow_http".into(), value);
1011 }
1012 if let Ok(value) = std::env::var("AWS_ALLOW_HTTP") {
1013 options.insert("allow_http".into(), value);
1014 }
1015 if let Ok(value) = std::env::var("OBJECT_STORE_CLIENT_MAX_RETRIES") {
1016 options.insert("client_max_retries".into(), value);
1017 }
1018 if let Ok(value) = std::env::var("OBJECT_STORE_CLIENT_RETRY_TIMEOUT") {
1019 options.insert("client_retry_timeout".into(), value);
1020 }
1021 Self(options)
1022 }
1023
1024 pub fn allow_http(&self) -> bool {
1026 self.0.iter().any(|(key, value)| {
1027 key.to_ascii_lowercase().contains("allow_http") & str_is_truthy(value)
1028 })
1029 }
1030
1031 pub fn download_retry_count(&self) -> usize {
1033 self.0
1034 .iter()
1035 .find(|(key, _)| key.eq_ignore_ascii_case("download_retry_count"))
1036 .map(|(_, value)| value.parse::<usize>().unwrap_or(3))
1037 .unwrap_or(3)
1038 }
1039
1040 pub fn client_max_retries(&self) -> usize {
1042 self.0
1043 .iter()
1044 .find(|(key, _)| key.eq_ignore_ascii_case("client_max_retries"))
1045 .and_then(|(_, value)| value.parse::<usize>().ok())
1046 .unwrap_or(3)
1047 }
1048
1049 pub fn client_retry_timeout(&self) -> u64 {
1051 self.0
1052 .iter()
1053 .find(|(key, _)| key.eq_ignore_ascii_case("client_retry_timeout"))
1054 .and_then(|(_, value)| value.parse::<u64>().ok())
1055 .unwrap_or(180)
1056 }
1057
1058 pub fn get(&self, key: &str) -> Option<&String> {
1059 self.0.get(key)
1060 }
1061
1062 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1070 pub fn client_options(&self) -> Result<ClientOptions> {
1071 let mut headers = HeaderMap::new();
1072 for (key, value) in &self.0 {
1073 if let Some(header_name) = key.strip_prefix("headers.") {
1074 let name = header_name
1075 .parse::<http::header::HeaderName>()
1076 .map_err(|e| {
1077 Error::invalid_input(format!("invalid header name '{header_name}': {e}"))
1078 })?;
1079 let val = HeaderValue::from_str(value).map_err(|e| {
1080 Error::invalid_input(format!("invalid header value for '{header_name}': {e}"))
1081 })?;
1082 headers.insert(name, val);
1083 }
1084 }
1085 let mut client_options = ClientOptions::default();
1086 if !headers.is_empty() {
1087 client_options = client_options.with_default_headers(headers);
1088 }
1089 Ok(client_options)
1090 }
1091
1092 pub fn expires_at_millis(&self) -> Option<u64> {
1094 self.0
1095 .get(EXPIRES_AT_MILLIS_KEY)
1096 .and_then(|s| s.parse::<u64>().ok())
1097 }
1098}
1099
1100impl From<HashMap<String, String>> for StorageOptions {
1101 fn from(value: HashMap<String, String>) -> Self {
1102 Self::new(value)
1103 }
1104}
1105
1106static DEFAULT_OBJECT_STORE_REGISTRY: std::sync::LazyLock<ObjectStoreRegistry> =
1107 std::sync::LazyLock::new(ObjectStoreRegistry::default);
1108
1109impl ObjectStore {
1110 #[allow(clippy::too_many_arguments)]
1111 pub fn new(
1112 store: Arc<DynObjectStore>,
1113 location: Url,
1114 block_size: Option<usize>,
1115 wrapper: Option<Arc<dyn WrappingObjectStore>>,
1116 use_constant_size_upload_parts: bool,
1117 list_is_lexically_ordered: bool,
1118 io_parallelism: usize,
1119 download_retry_count: usize,
1120 storage_options: Option<&HashMap<String, String>>,
1121 ) -> Self {
1122 let scheme = location.scheme();
1123 let block_size = block_size.unwrap_or_else(|| infer_block_size(scheme));
1124 let store_prefix = match DEFAULT_OBJECT_STORE_REGISTRY.get_provider(scheme) {
1125 Some(provider) => provider
1126 .calculate_object_store_prefix(&location, storage_options)
1127 .unwrap(),
1128 None => {
1129 let store_prefix = format!("{}${}", location.scheme(), location.authority());
1130 log::warn!(
1131 "Guessing that object store prefix is {}, since object store scheme is not found in registry.",
1132 store_prefix
1133 );
1134 store_prefix
1135 }
1136 };
1137 let store = match wrapper {
1138 Some(wrapper) => wrapper.wrap(&store_prefix, store),
1139 None => store,
1140 };
1141
1142 let io_tracker = IOTracker::default();
1144 let tracked_store = io_tracker.wrap("", store);
1145
1146 Self {
1147 inner: tracked_store,
1148 scheme: scheme.into(),
1149 block_size,
1150 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
1151 use_constant_size_upload_parts,
1152 list_is_lexically_ordered,
1153 io_parallelism,
1154 download_retry_count,
1155 io_tracker,
1156 store_prefix,
1157 }
1158 }
1159}
1160
1161fn infer_block_size(scheme: &str) -> usize {
1162 match scheme {
1166 "file" => 4 * 1024,
1167 _ => 64 * 1024,
1168 }
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173 use super::*;
1174 use async_trait::async_trait;
1175 use bytes::Bytes;
1176 use lance_core::utils::tempfile::{TempStdDir, TempStdFile, TempStrDir};
1177 use object_store::memory::InMemory;
1178 use object_store::{
1179 CopyOptions, GetOptions, GetResult, ListResult, MultipartUpload, PutMultipartOptions,
1180 PutOptions, PutPayload, PutResult, Result as OSResult,
1181 };
1182 use rstest::rstest;
1183 use std::env::set_current_dir;
1184 use std::fmt::{Display, Formatter};
1185 use std::fs::{create_dir_all, write};
1186 use std::ops::Range;
1187 use std::path::Path as StdPath;
1188 use std::sync::atomic::{AtomicBool, Ordering};
1189
1190 fn write_to_file(path_str: &str, contents: &str) -> std::io::Result<()> {
1192 let path = expand_path(path_str).map_err(std::io::Error::other)?;
1193 std::fs::create_dir_all(path.parent().unwrap())?;
1194 write(path, contents)
1195 }
1196
1197 async fn read_from_store(store: &ObjectStore, path: &Path) -> Result<String> {
1198 let test_file_store = store.open(path).await.unwrap();
1199 let size = test_file_store.size().await.unwrap();
1200 let bytes = test_file_store.get_range(0..size).await.unwrap();
1201 let contents = String::from_utf8(bytes.to_vec()).unwrap();
1202 Ok(contents)
1203 }
1204
1205 #[test]
1206 fn test_io_parallelism_clamped_to_nonzero() {
1207 let store = ObjectStore::local();
1210
1211 unsafe { std::env::set_var("LANCE_IO_THREADS", "0") };
1214 assert_eq!(
1215 store.io_parallelism(),
1216 1,
1217 "LANCE_IO_THREADS=0 must clamp to 1"
1218 );
1219
1220 unsafe { std::env::set_var("LANCE_IO_THREADS", "8") };
1221 assert_eq!(
1222 store.io_parallelism(),
1223 8,
1224 "a positive override must pass through unchanged"
1225 );
1226
1227 unsafe { std::env::remove_var("LANCE_IO_THREADS") };
1228 assert!(
1229 store.io_parallelism() >= 1,
1230 "the configured default parallelism must be at least 1"
1231 );
1232 }
1233
1234 #[tokio::test]
1235 async fn test_absolute_paths() {
1236 let tmp_path = TempStrDir::default();
1237 write_to_file(
1238 &format!("{tmp_path}/bar/foo.lance/test_file"),
1239 "TEST_CONTENT",
1240 )
1241 .unwrap();
1242
1243 for uri in &[
1245 format!("{tmp_path}/bar/foo.lance"),
1246 format!("{tmp_path}/./bar/foo.lance"),
1247 format!("{tmp_path}/bar/foo.lance/../foo.lance"),
1248 ] {
1249 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
1250 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
1251 .await
1252 .unwrap();
1253 assert_eq!(contents, "TEST_CONTENT");
1254 }
1255 }
1256
1257 #[tokio::test]
1258 async fn test_cloud_paths() {
1259 let uri = "s3://bucket/foo.lance";
1260 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
1261 assert_eq!(store.scheme, "s3");
1262 assert_eq!(path.to_string(), "foo.lance");
1263
1264 let (store, path) = ObjectStore::from_uri("s3+ddb://bucket/foo.lance")
1265 .await
1266 .unwrap();
1267 assert_eq!(store.scheme, "s3");
1268 assert_eq!(path.to_string(), "foo.lance");
1269
1270 let (store, path) = ObjectStore::from_uri("gs://bucket/foo.lance")
1271 .await
1272 .unwrap();
1273 assert_eq!(store.scheme, "gs");
1274 assert_eq!(path.to_string(), "foo.lance");
1275
1276 let (store, path) =
1277 ObjectStore::from_uri("abfss://filesystem@account.dfs.core.windows.net/foo.lance")
1278 .await
1279 .unwrap();
1280 assert_eq!(store.scheme, "abfss");
1281 assert_eq!(path.to_string(), "foo.lance");
1282 }
1283
1284 async fn test_block_size_used_test_helper(
1285 uri: &str,
1286 storage_options: Option<HashMap<String, String>>,
1287 default_expected_block_size: usize,
1288 ) {
1289 let registry = Arc::new(ObjectStoreRegistry::default());
1291 let accessor = storage_options
1292 .clone()
1293 .map(|opts| Arc::new(StorageOptionsAccessor::with_static_options(opts)));
1294 let params = ObjectStoreParams {
1295 storage_options_accessor: accessor.clone(),
1296 ..ObjectStoreParams::default()
1297 };
1298 let (store, _) = ObjectStore::from_uri_and_params(registry, uri, ¶ms)
1299 .await
1300 .unwrap();
1301 assert_eq!(store.block_size, default_expected_block_size);
1302
1303 let registry = Arc::new(ObjectStoreRegistry::default());
1305 let params = ObjectStoreParams {
1306 block_size: Some(1024),
1307 storage_options_accessor: accessor,
1308 ..ObjectStoreParams::default()
1309 };
1310 let (store, _) = ObjectStore::from_uri_and_params(registry, uri, ¶ms)
1311 .await
1312 .unwrap();
1313 assert_eq!(store.block_size, 1024);
1314 }
1315
1316 #[rstest]
1317 #[case("s3://bucket/foo.lance", None)]
1318 #[case("gs://bucket/foo.lance", None)]
1319 #[case("az://account/bucket/foo.lance",
1320 Some(HashMap::from([
1321 (String::from("account_name"), String::from("account")),
1322 (String::from("container_name"), String::from("container"))
1323 ])))]
1324 #[case("abfss://filesystem@account.dfs.core.windows.net/foo.lance",
1325 Some(HashMap::from([
1326 (String::from("account_name"), String::from("account")),
1327 (String::from("container_name"), String::from("filesystem"))
1328 ])))]
1329 #[tokio::test]
1330 async fn test_block_size_used_cloud(
1331 #[case] uri: &str,
1332 #[case] storage_options: Option<HashMap<String, String>>,
1333 ) {
1334 test_block_size_used_test_helper(uri, storage_options, 64 * 1024).await;
1335 }
1336
1337 #[rstest]
1338 #[case("file")]
1339 #[case("file-object-store")]
1340 #[case("memory:///bucket/foo.lance")]
1341 #[tokio::test]
1342 async fn test_block_size_used_file(#[case] prefix: &str) {
1343 let tmp_path = TempStrDir::default();
1344 let path = format!("{tmp_path}/bar/foo.lance/test_file");
1345 write_to_file(&path, "URL").unwrap();
1346 let uri = format!("{prefix}:///{path}");
1347 test_block_size_used_test_helper(&uri, None, 4 * 1024).await;
1348 }
1349
1350 #[tokio::test]
1351 async fn test_relative_paths() {
1352 let tmp_path = TempStrDir::default();
1353 write_to_file(
1354 &format!("{tmp_path}/bar/foo.lance/test_file"),
1355 "RELATIVE_URL",
1356 )
1357 .unwrap();
1358
1359 set_current_dir(StdPath::new(tmp_path.as_ref())).expect("Error changing current dir");
1360 let (store, path) = ObjectStore::from_uri("./bar/foo.lance").await.unwrap();
1361
1362 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
1363 .await
1364 .unwrap();
1365 assert_eq!(contents, "RELATIVE_URL");
1366 }
1367
1368 #[tokio::test]
1369 async fn test_tilde_expansion() {
1370 let uri = "~/foo.lance";
1371 write_to_file(&format!("{uri}/test_file"), "TILDE").unwrap();
1372 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
1373 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
1374 .await
1375 .unwrap();
1376 assert_eq!(contents, "TILDE");
1377 }
1378
1379 #[tokio::test]
1380 async fn test_read_directory() {
1381 let path = TempStdDir::default();
1382 create_dir_all(path.join("foo").join("bar")).unwrap();
1383 create_dir_all(path.join("foo").join("zoo")).unwrap();
1384 create_dir_all(path.join("foo").join("zoo").join("abc")).unwrap();
1385 write_to_file(
1386 path.join("foo").join("test_file").to_str().unwrap(),
1387 "read_dir",
1388 )
1389 .unwrap();
1390 let (store, base) = ObjectStore::from_uri(path.to_str().unwrap()).await.unwrap();
1391
1392 let sub_dirs = store.read_dir(base.clone().join("foo")).await.unwrap();
1393 assert_eq!(sub_dirs, vec!["bar", "zoo", "test_file"]);
1394 }
1395
1396 #[tokio::test]
1397 async fn test_delete_directory_local_store() {
1398 test_delete_directory("").await;
1399 }
1400
1401 #[tokio::test]
1402 async fn test_delete_directory_file_object_store() {
1403 test_delete_directory("file-object-store").await;
1404 }
1405
1406 async fn test_delete_directory(scheme: &str) {
1407 let path = TempStdDir::default();
1408 create_dir_all(path.join("foo").join("bar")).unwrap();
1409 create_dir_all(path.join("foo").join("zoo")).unwrap();
1410 create_dir_all(path.join("foo").join("zoo").join("abc")).unwrap();
1411 write_to_file(
1412 path.join("foo")
1413 .join("bar")
1414 .join("test_file")
1415 .to_str()
1416 .unwrap(),
1417 "delete",
1418 )
1419 .unwrap();
1420 let file_url = Url::from_directory_path(&path).unwrap();
1421 let url = if scheme.is_empty() {
1422 file_url
1423 } else {
1424 let mut url = Url::parse(&format!("{scheme}:///")).unwrap();
1425 url.set_path(file_url.path());
1427 url
1428 };
1429 let (store, base) = ObjectStore::from_uri(url.as_ref()).await.unwrap();
1430 store
1431 .remove_dir_all(base.clone().join("foo"))
1432 .await
1433 .unwrap();
1434
1435 assert!(!path.join("foo").exists());
1436 }
1437
1438 #[derive(Debug)]
1439 struct TestWrapper {
1440 called: AtomicBool,
1441
1442 return_value: Arc<dyn OSObjectStore>,
1443 }
1444
1445 impl WrappingObjectStore for TestWrapper {
1446 fn wrap(
1447 &self,
1448 _store_prefix: &str,
1449 _original: Arc<dyn OSObjectStore>,
1450 ) -> Arc<dyn OSObjectStore> {
1451 self.called.store(true, Ordering::Relaxed);
1452
1453 self.return_value.clone()
1455 }
1456 }
1457
1458 impl TestWrapper {
1459 fn called(&self) -> bool {
1460 self.called.load(Ordering::Relaxed)
1461 }
1462 }
1463
1464 #[tokio::test]
1465 async fn test_wrapping_object_store_option_is_used() {
1466 let mock_inner_store: Arc<dyn OSObjectStore> = Arc::new(InMemory::new());
1468 let registry = Arc::new(ObjectStoreRegistry::default());
1469
1470 assert_eq!(Arc::strong_count(&mock_inner_store), 1);
1471
1472 let wrapper = Arc::new(TestWrapper {
1473 called: AtomicBool::new(false),
1474 return_value: mock_inner_store.clone(),
1475 });
1476
1477 let params = ObjectStoreParams {
1478 object_store_wrapper: Some(wrapper.clone()),
1479 ..ObjectStoreParams::default()
1480 };
1481
1482 assert!(!wrapper.called());
1484
1485 let _ = ObjectStore::from_uri_and_params(registry, "memory:///", ¶ms)
1486 .await
1487 .unwrap();
1488
1489 assert!(wrapper.called());
1491
1492 assert_eq!(Arc::strong_count(&mock_inner_store), 2);
1495 }
1496
1497 #[tokio::test]
1498 async fn test_local_paths() {
1499 let file_path = TempStdFile::default();
1500 let mut writer = ObjectStore::create_local_writer(&file_path).await.unwrap();
1501 writer.write_all(b"LOCAL").await.unwrap();
1502 Writer::shutdown(&mut writer).await.unwrap();
1503
1504 let reader = ObjectStore::open_local(&file_path).await.unwrap();
1505 let buf = reader.get_range(0..5).await.unwrap();
1506 assert_eq!(buf.as_ref(), b"LOCAL");
1507 }
1508
1509 #[tokio::test]
1510 async fn test_read_one() {
1511 let file_path = TempStdFile::default();
1512 let mut writer = ObjectStore::create_local_writer(&file_path).await.unwrap();
1513 writer.write_all(b"LOCAL").await.unwrap();
1514 Writer::shutdown(&mut writer).await.unwrap();
1515
1516 let file_path_os = object_store::path::Path::parse(file_path.to_str().unwrap()).unwrap();
1517 let obj_store = ObjectStore::local();
1518 let buf = obj_store.read_one_all(&file_path_os).await.unwrap();
1519 assert_eq!(buf.as_ref(), b"LOCAL");
1520
1521 let buf = obj_store.read_one_range(&file_path_os, 0..5).await.unwrap();
1522 assert_eq!(buf.as_ref(), b"LOCAL");
1523 }
1524
1525 #[tokio::test]
1526 #[cfg(windows)]
1527 async fn test_windows_paths() {
1528 use std::path::Component;
1529 use std::path::Prefix;
1530 use std::path::Prefix::*;
1531
1532 fn get_path_prefix(path: &StdPath) -> Prefix<'_> {
1533 match path.components().next().unwrap() {
1534 Component::Prefix(prefix_component) => prefix_component.kind(),
1535 _ => panic!(),
1536 }
1537 }
1538
1539 fn get_drive_letter(prefix: Prefix) -> String {
1540 match prefix {
1541 Disk(bytes) => String::from_utf8(vec![bytes]).unwrap(),
1542 _ => panic!(),
1543 }
1544 }
1545
1546 let tmp_path = TempStdFile::default();
1547 let prefix = get_path_prefix(&tmp_path);
1548 let drive_letter = get_drive_letter(prefix);
1549
1550 write_to_file(
1551 &(format!("{drive_letter}:/test_folder/test.lance") + "/test_file"),
1552 "WINDOWS",
1553 )
1554 .unwrap();
1555
1556 for uri in &[
1557 format!("{drive_letter}:/test_folder/test.lance"),
1558 format!("{drive_letter}:\\test_folder\\test.lance"),
1559 ] {
1560 let (store, base) = ObjectStore::from_uri(uri).await.unwrap();
1561 let contents = read_from_store(store.as_ref(), &base.clone().join("test_file"))
1562 .await
1563 .unwrap();
1564 assert_eq!(contents, "WINDOWS");
1565 }
1566 }
1567
1568 #[tokio::test]
1569 async fn test_cross_filesystem_copy() {
1570 let source_dir = TempStdDir::default();
1572 let dest_dir = TempStdDir::default();
1573
1574 let source_file_name = "test_file.txt";
1576 let source_file = source_dir.join(source_file_name);
1577 std::fs::write(&source_file, b"test content").unwrap();
1578
1579 let (store, base_path) = ObjectStore::from_uri(source_dir.to_str().unwrap())
1581 .await
1582 .unwrap();
1583
1584 let from_path = base_path.clone().join(source_file_name);
1586
1587 let dest_file = dest_dir.join("copied_file.txt");
1589 let dest_str = dest_file.to_str().unwrap();
1590 let to_path = object_store::path::Path::parse(dest_str).unwrap();
1591
1592 store.copy(&from_path, &to_path).await.unwrap();
1594
1595 assert!(dest_file.exists());
1597 let copied_content = std::fs::read(&dest_file).unwrap();
1598 assert_eq!(copied_content, b"test content");
1599 }
1600
1601 #[tokio::test]
1602 async fn test_copy_creates_parent_directories() {
1603 let source_dir = TempStdDir::default();
1604 let dest_dir = TempStdDir::default();
1605
1606 let source_file_name = "test_file.txt";
1608 let source_file = source_dir.join(source_file_name);
1609 std::fs::write(&source_file, b"test content").unwrap();
1610
1611 let (store, base_path) = ObjectStore::from_uri(source_dir.to_str().unwrap())
1613 .await
1614 .unwrap();
1615
1616 let from_path = base_path.clone().join(source_file_name);
1618
1619 let dest_file = dest_dir.join("nested").join("dirs").join("copied_file.txt");
1621 let dest_str = dest_file.to_str().unwrap();
1622 let to_path = object_store::path::Path::parse(dest_str).unwrap();
1623
1624 store.copy(&from_path, &to_path).await.unwrap();
1626
1627 assert!(dest_file.exists());
1629 assert!(dest_file.parent().unwrap().exists());
1630 let copied_content = std::fs::read(&dest_file).unwrap();
1631 assert_eq!(copied_content, b"test content");
1632 }
1633
1634 #[derive(Debug)]
1639 struct CopyFailingStore {
1640 inner: InMemory,
1641 }
1642
1643 impl Display for CopyFailingStore {
1644 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1645 write!(f, "CopyFailingStore")
1646 }
1647 }
1648
1649 #[async_trait]
1650 impl OSObjectStore for CopyFailingStore {
1651 async fn put_opts(
1652 &self,
1653 location: &Path,
1654 bytes: PutPayload,
1655 opts: PutOptions,
1656 ) -> OSResult<PutResult> {
1657 self.inner.put_opts(location, bytes, opts).await
1658 }
1659 async fn put_multipart_opts(
1660 &self,
1661 location: &Path,
1662 opts: PutMultipartOptions,
1663 ) -> OSResult<Box<dyn MultipartUpload>> {
1664 self.inner.put_multipart_opts(location, opts).await
1665 }
1666 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
1667 self.inner.get_opts(location, options).await
1668 }
1669 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
1670 self.inner.get_ranges(location, ranges).await
1671 }
1672 fn delete_stream(
1673 &self,
1674 locations: BoxStream<'static, OSResult<Path>>,
1675 ) -> BoxStream<'static, OSResult<Path>> {
1676 self.inner.delete_stream(locations)
1677 }
1678 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
1679 self.inner.list(prefix)
1680 }
1681 fn list_with_offset(
1682 &self,
1683 prefix: Option<&Path>,
1684 offset: &Path,
1685 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
1686 self.inner.list_with_offset(prefix, offset)
1687 }
1688 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
1689 self.inner.list_with_delimiter(prefix).await
1690 }
1691 async fn copy_opts(&self, _from: &Path, _to: &Path, _opts: CopyOptions) -> OSResult<()> {
1692 Err(object_store::Error::Generic {
1693 store: "CopyFailingStore",
1694 source: "single-shot copy disabled in test".into(),
1695 })
1696 }
1697 }
1698
1699 #[tokio::test]
1700 async fn test_copy_streams_objects_larger_than_threshold() {
1701 let mut store = ObjectStore::memory();
1707 store.inner = Arc::new(CopyFailingStore {
1708 inner: InMemory::new(),
1709 });
1710
1711 let from = Path::from("source.bin");
1712 let contents = b"streaming multipart copy payload well past the tiny threshold";
1713 store.put(&from, contents).await.unwrap();
1714
1715 let streamed = Path::from("streamed.bin");
1718 store.copy_impl(&from, &streamed, true, 8).await.unwrap();
1719 let copied = store.read_one_all(&streamed).await.unwrap();
1720 assert_eq!(copied.as_ref(), contents.as_slice());
1721
1722 let native = Path::from("native.bin");
1726 assert!(
1727 store
1728 .copy_impl(&from, &native, true, u64::MAX)
1729 .await
1730 .is_err()
1731 );
1732 }
1733
1734 #[test]
1735 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1736 fn test_client_options_extracts_headers() {
1737 let opts = StorageOptions(HashMap::from([
1738 ("headers.x-custom-foo".to_string(), "bar".to_string()),
1739 ("headers.x-ms-version".to_string(), "2023-11-03".to_string()),
1740 ("region".to_string(), "us-west-2".to_string()),
1741 ]));
1742 let client_options = opts.client_options().unwrap();
1743
1744 let opts_no_headers = StorageOptions(HashMap::from([(
1747 "region".to_string(),
1748 "us-west-2".to_string(),
1749 )]));
1750 opts_no_headers.client_options().unwrap();
1751
1752 #[cfg(feature = "gcp")]
1756 {
1757 use object_store::gcp::GoogleCloudStorageBuilder;
1758 let _builder = GoogleCloudStorageBuilder::new()
1759 .with_client_options(client_options)
1760 .with_url("gs://test-bucket");
1761 }
1762 }
1763
1764 #[test]
1765 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1766 fn test_client_options_rejects_invalid_header_name() {
1767 let opts = StorageOptions(HashMap::from([(
1768 "headers.bad header".to_string(),
1769 "value".to_string(),
1770 )]));
1771 let err = opts.client_options().unwrap_err();
1772 assert!(err.to_string().contains("invalid header name"));
1773 }
1774
1775 #[test]
1776 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1777 fn test_client_options_rejects_invalid_header_value() {
1778 let opts = StorageOptions(HashMap::from([(
1779 "headers.x-good-name".to_string(),
1780 "bad\x01value".to_string(),
1781 )]));
1782 let err = opts.client_options().unwrap_err();
1783 assert!(err.to_string().contains("invalid header value"));
1784 }
1785
1786 #[test]
1787 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1788 fn test_client_options_empty_when_no_header_keys() {
1789 let opts = StorageOptions(HashMap::from([
1790 ("region".to_string(), "us-east-1".to_string()),
1791 ("access_key_id".to_string(), "AKID".to_string()),
1792 ]));
1793 opts.client_options().unwrap();
1794 }
1795}