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
289fn wrapper_allocation_ptr(wrapper: &Arc<dyn WrappingObjectStore>) -> *const () {
290 Arc::as_ptr(wrapper) as *const ()
293}
294
295impl std::hash::Hash for ObjectStoreParams {
297 #[allow(deprecated)]
298 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
299 self.block_size.hash(state);
301 if let Some((store, url)) = &self.object_store {
302 Arc::as_ptr(store).hash(state);
303 url.hash(state);
304 }
305 self.s3_credentials_refresh_offset.hash(state);
306 #[cfg(feature = "aws")]
307 if let Some(aws_credentials) = &self.aws_credentials {
308 Arc::as_ptr(aws_credentials).hash(state);
309 }
310 if let Some(wrapper) = &self.object_store_wrapper {
311 wrapper_allocation_ptr(wrapper).hash(state);
312 }
313 if let Some(accessor) = &self.storage_options_accessor {
314 accessor.accessor_id().hash(state);
315 }
316 self.use_constant_size_upload_parts.hash(state);
317 self.list_is_lexically_ordered.hash(state);
318 }
319}
320
321impl Eq for ObjectStoreParams {}
323impl PartialEq for ObjectStoreParams {
324 #[allow(deprecated)]
325 fn eq(&self, other: &Self) -> bool {
326 #[cfg(feature = "aws")]
327 if self.aws_credentials.is_some() != other.aws_credentials.is_some() {
328 return false;
329 }
330
331 self.block_size == other.block_size
334 && self
335 .object_store
336 .as_ref()
337 .map(|(store, url)| (Arc::as_ptr(store), url))
338 == other
339 .object_store
340 .as_ref()
341 .map(|(store, url)| (Arc::as_ptr(store), url))
342 && self.s3_credentials_refresh_offset == other.s3_credentials_refresh_offset
343 && self
344 .object_store_wrapper
345 .as_ref()
346 .map(wrapper_allocation_ptr)
347 == other
348 .object_store_wrapper
349 .as_ref()
350 .map(wrapper_allocation_ptr)
351 && self
352 .storage_options_accessor
353 .as_ref()
354 .map(|a| a.accessor_id())
355 == other
356 .storage_options_accessor
357 .as_ref()
358 .map(|a| a.accessor_id())
359 && self.use_constant_size_upload_parts == other.use_constant_size_upload_parts
360 && self.list_is_lexically_ordered == other.list_is_lexically_ordered
361 }
362}
363
364pub fn uri_to_url(uri: &str) -> Result<Url> {
385 match Url::parse(uri) {
386 Ok(url) if url.scheme().len() == 1 && cfg!(windows) => {
387 local_path_to_url(uri)
389 }
390 Ok(url) => Ok(url),
391 Err(_) => local_path_to_url(uri),
392 }
393}
394
395fn expand_path(str_path: impl AsRef<str>) -> Result<std::path::PathBuf> {
396 let str_path = str_path.as_ref();
397 let expanded = expand_tilde_path(str_path).unwrap_or_else(|| str_path.into());
398
399 let mut expanded_path = path_abs::PathAbs::new(expanded)
400 .unwrap()
401 .as_path()
402 .to_path_buf();
403 if let Some(s) = expanded_path.as_path().to_str()
405 && s.is_empty()
406 {
407 expanded_path = std::env::current_dir()?;
408 }
409
410 Ok(expanded_path)
411}
412
413fn expand_tilde_path(path: &str) -> Option<std::path::PathBuf> {
414 let home_dir = std::env::home_dir()?;
415 if path == "~" {
416 return Some(home_dir);
417 }
418 if let Some(stripped) = path.strip_prefix("~/") {
419 return Some(home_dir.join(stripped));
420 }
421 #[cfg(windows)]
422 if let Some(stripped) = path.strip_prefix("~\\") {
423 return Some(home_dir.join(stripped));
424 }
425
426 None
427}
428
429fn local_path_to_url(str_path: &str) -> Result<Url> {
430 let expanded_path = expand_path(str_path)?;
431
432 Url::from_directory_path(expanded_path).map_err(|_| {
433 Error::invalid_input_source(format!("Invalid table location: '{}'", str_path).into())
434 })
435}
436
437#[cfg(feature = "huggingface")]
438fn parse_hf_repo_id(url: &Url) -> Result<String> {
439 let mut segments: Vec<String> = Vec::new();
441 if let Some(host) = url.host_str() {
442 segments.push(host.to_string());
443 }
444 segments.extend(
445 url.path()
446 .trim_start_matches('/')
447 .split('/')
448 .map(|s| s.to_string()),
449 );
450
451 if segments.len() < 2 {
452 return Err(Error::invalid_input(
453 "Huggingface URL must contain at least owner and repo",
454 ));
455 }
456
457 let repo_type_candidates = ["models", "datasets", "spaces"];
458 let (owner, repo_with_rev) = if repo_type_candidates.contains(&segments[0].as_str()) {
459 if segments.len() < 3 {
460 return Err(Error::invalid_input(
461 "Huggingface URL missing owner/repo after repo type",
462 ));
463 }
464 (segments[1].as_str(), segments[2].as_str())
465 } else {
466 (segments[0].as_str(), segments[1].as_str())
467 };
468
469 let repo = repo_with_rev
470 .split_once('@')
471 .map(|(r, _)| r)
472 .unwrap_or(repo_with_rev);
473 Ok(format!("{owner}/{repo}"))
474}
475
476impl ObjectStore {
477 pub async fn from_uri(uri: &str) -> Result<(Arc<Self>, Path)> {
485 let registry = Arc::new(ObjectStoreRegistry::default());
486
487 Self::from_uri_and_params(registry, uri, &ObjectStoreParams::default()).await
488 }
489
490 pub async fn from_uri_and_params(
494 registry: Arc<ObjectStoreRegistry>,
495 uri: &str,
496 params: &ObjectStoreParams,
497 ) -> Result<(Arc<Self>, Path)> {
498 #[allow(deprecated)]
499 if let Some((store, path)) = params.object_store.as_ref() {
500 let mut inner = store.clone();
501 let store_prefix =
502 registry.calculate_object_store_prefix(uri, params.storage_options())?;
503 if let Some(wrapper) = params.object_store_wrapper.as_ref() {
504 inner = wrapper.wrap(&store_prefix, inner);
505 }
506
507 let io_tracker = IOTracker::default();
509 let tracked_store = io_tracker.wrap("", inner);
510
511 let store = Self {
512 inner: tracked_store,
513 scheme: path.scheme().to_string(),
514 block_size: params.block_size.unwrap_or(64 * 1024),
515 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
516 use_constant_size_upload_parts: params.use_constant_size_upload_parts,
517 list_is_lexically_ordered: params.list_is_lexically_ordered.unwrap_or_default(),
518 io_parallelism: DEFAULT_CLOUD_IO_PARALLELISM,
519 download_retry_count: DEFAULT_DOWNLOAD_RETRY_COUNT,
520 io_tracker,
521 store_prefix,
522 };
523 let path = Path::parse(path.path())?;
524 return Ok((Arc::new(store), path));
525 }
526 let url = uri_to_url(uri)?;
527
528 let store = registry.get_store(url.clone(), params).await?;
529 let provider = registry.get_provider(url.scheme()).expect_ok()?;
531 let path = provider.extract_path(&url)?;
532
533 Ok((store, path))
534 }
535
536 pub fn extract_path_from_uri(registry: Arc<ObjectStoreRegistry>, uri: &str) -> Result<Path> {
550 let url = uri_to_url(uri)?;
551 let provider = registry
552 .get_provider(url.scheme())
553 .ok_or_else(|| Error::invalid_input(format!("Unknown scheme: {}", url.scheme())))?;
554 provider.extract_path(&url)
555 }
556
557 #[deprecated(note = "Use `from_uri` instead")]
558 pub fn from_path(str_path: &str) -> Result<(Arc<Self>, Path)> {
559 Self::from_uri_and_params(
560 Arc::new(ObjectStoreRegistry::default()),
561 str_path,
562 &Default::default(),
563 )
564 .now_or_never()
565 .unwrap()
566 }
567
568 pub fn local() -> Self {
570 let provider = FileStoreProvider;
571 provider
572 .new_store(Url::parse("file:///").unwrap(), &Default::default())
573 .now_or_never()
574 .unwrap()
575 .unwrap()
576 }
577
578 pub fn memory() -> Self {
580 let provider = MemoryStoreProvider;
581 provider
582 .new_store(Url::parse("memory:///").unwrap(), &Default::default())
583 .now_or_never()
584 .unwrap()
585 .unwrap()
586 }
587
588 pub fn is_local(&self) -> bool {
590 self.scheme == "file" || self.scheme == "file+uring"
591 }
592
593 pub fn is_cloud(&self) -> bool {
594 if self.is_local() || self.scheme == "memory" || self.scheme == "shared-memory" {
595 return false;
596 }
597 true
598 }
599
600 pub fn prefers_lite_scheduler(&self) -> bool {
605 self.scheme == "file+uring"
606 }
607
608 pub fn scheme(&self) -> &str {
609 &self.scheme
610 }
611
612 pub fn block_size(&self) -> usize {
613 self.block_size
614 }
615
616 pub fn max_iop_size(&self) -> u64 {
617 self.max_iop_size
618 }
619
620 pub fn io_parallelism(&self) -> usize {
627 std::env::var("LANCE_IO_THREADS")
628 .map(|val| val.parse::<usize>().unwrap())
629 .unwrap_or(self.io_parallelism)
630 .max(1)
631 }
632
633 pub fn io_tracker(&self) -> &IOTracker {
638 &self.io_tracker
639 }
640
641 pub fn io_stats_snapshot(&self) -> IoStats {
646 self.io_tracker.stats()
647 }
648
649 pub fn io_stats_incremental(&self) -> IoStats {
655 self.io_tracker.incremental_stats()
656 }
657
658 pub async fn open(&self, path: &Path) -> Result<Box<dyn Reader>> {
663 match self.scheme.as_str() {
664 "file" => {
665 LocalObjectReader::open_with_tracker(
666 path,
667 self.block_size,
668 None,
669 Arc::new(self.io_tracker.clone()),
670 )
671 .await
672 }
673 #[cfg(target_os = "linux")]
674 "file+uring" => {
675 let use_current_thread = std::env::var("LANCE_URING_CURRENT_THREAD")
677 .map(|v| str_is_truthy(&v))
678 .unwrap_or(false);
679
680 if use_current_thread {
681 UringCurrentThreadReader::open(
682 path,
683 self.block_size,
684 None,
685 Arc::new(self.io_tracker.clone()),
686 )
687 .await
688 } else {
689 UringReader::open(
690 path,
691 self.block_size,
692 None,
693 Arc::new(self.io_tracker.clone()),
694 )
695 .await
696 }
697 }
698 _ => Ok(Box::new(CloudObjectReader::new(
699 self.inner.clone(),
700 path.clone(),
701 self.block_size,
702 None,
703 self.download_retry_count,
704 )?)),
705 }
706 }
707
708 pub async fn open_with_size(&self, path: &Path, known_size: usize) -> Result<Box<dyn Reader>> {
714 if known_size <= self.block_size {
717 return Ok(Box::new(SmallReader::new(
718 self.inner.clone(),
719 path.clone(),
720 self.download_retry_count,
721 known_size,
722 )));
723 }
724
725 match self.scheme.as_str() {
726 "file" => {
727 LocalObjectReader::open_with_tracker(
728 path,
729 self.block_size,
730 Some(known_size),
731 Arc::new(self.io_tracker.clone()),
732 )
733 .await
734 }
735 #[cfg(target_os = "linux")]
736 "file+uring" => {
737 let use_current_thread = std::env::var("LANCE_URING_CURRENT_THREAD")
739 .map(|v| str_is_truthy(&v))
740 .unwrap_or(false);
741
742 if use_current_thread {
743 UringCurrentThreadReader::open(
744 path,
745 self.block_size,
746 Some(known_size),
747 Arc::new(self.io_tracker.clone()),
748 )
749 .await
750 } else {
751 UringReader::open(
752 path,
753 self.block_size,
754 Some(known_size),
755 Arc::new(self.io_tracker.clone()),
756 )
757 .await
758 }
759 }
760 _ => Ok(Box::new(CloudObjectReader::new(
761 self.inner.clone(),
762 path.clone(),
763 self.block_size,
764 Some(known_size),
765 self.download_retry_count,
766 )?)),
767 }
768 }
769
770 pub async fn create_local_writer(path: &std::path::Path) -> Result<ObjectWriter> {
772 let object_store = Self::local();
773 let absolute_path = expand_path(path.to_string_lossy())?;
774 let os_path = Path::from_absolute_path(absolute_path)?;
775 ObjectWriter::new(&object_store, &os_path).await
776 }
777
778 pub async fn open_local(path: &std::path::Path) -> Result<Box<dyn Reader>> {
780 let object_store = Self::local();
781 let absolute_path = expand_path(path.to_string_lossy())?;
782 let os_path = Path::from_absolute_path(absolute_path)?;
783 object_store.open(&os_path).await
784 }
785
786 pub async fn create(&self, path: &Path) -> Result<Box<dyn Writer>> {
788 match self.scheme.as_str() {
789 "file" => {
790 let local_path = super::local::to_local_path(path);
791 let local_path = std::path::PathBuf::from(&local_path);
792 if let Some(parent) = local_path.parent() {
793 tokio::fs::create_dir_all(parent).await?;
794 }
795 let parent = local_path
796 .parent()
797 .expect("file path must have parent")
798 .to_owned();
799 let named_temp =
800 tokio::task::spawn_blocking(move || tempfile::NamedTempFile::new_in(parent))
801 .await
802 .map_err(|e| Error::io(format!("spawn_blocking failed: {}", e)))??;
803 let (std_file, temp_path) = named_temp.into_parts();
804 let file = tokio::fs::File::from_std(std_file);
805 Ok(Box::new(LocalWriter::new(
806 file,
807 path.clone(),
808 temp_path,
809 Arc::new(self.io_tracker.clone()),
810 )))
811 }
812 _ => Ok(Box::new(ObjectWriter::new(self, path).await?)),
813 }
814 }
815
816 pub async fn put(&self, path: &Path, content: &[u8]) -> Result<WriteResult> {
818 let mut writer = self.create(path).await?;
819 writer.write_all(content).await?;
820 Writer::shutdown(writer.as_mut()).await
821 }
822
823 pub async fn delete(&self, path: &Path) -> Result<()> {
824 self.inner.delete(path).await?;
825 Ok(())
826 }
827
828 const MAX_SINGLE_COPY_BYTES: u64 = 5 * 1024 * 1024 * 1024; pub async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
833 let multipart_copy_fallback = matches!(self.scheme.as_str(), "s3" | "s3+ddb" | "gs");
839 self.copy_impl(
840 from,
841 to,
842 multipart_copy_fallback,
843 Self::MAX_SINGLE_COPY_BYTES,
844 )
845 .await
846 }
847
848 async fn copy_impl(
854 &self,
855 from: &Path,
856 to: &Path,
857 multipart_copy_fallback: bool,
858 max_single_copy: u64,
859 ) -> Result<()> {
860 if self.is_local() {
861 return super::local::copy_file(from, to);
863 }
864 if multipart_copy_fallback {
865 let reader = self.open(from).await?;
868 if reader.size().await? as u64 > max_single_copy {
869 let mut writer = self.create(to).await?;
870 writer.copy_from_reader(reader.as_ref()).await?;
871 Writer::shutdown(writer.as_mut()).await?;
872 return Ok(());
873 }
874 }
875 Ok(self.inner.copy(from, to).await?)
876 }
877
878 pub async fn read_dir(&self, dir_path: impl Into<Path>) -> Result<Vec<String>> {
880 let path = dir_path.into();
881 let path = Path::parse(&path)?;
882 let output = self.inner.list_with_delimiter(Some(&path)).await?;
883 Ok(output
884 .common_prefixes
885 .iter()
886 .chain(output.objects.iter().map(|o| &o.location))
887 .filter_map(|s| s.filename().map(|f| f.to_string()))
888 .collect())
889 }
890
891 pub async fn list_with_delimiter(&self, prefix: Option<&Path>) -> Result<ListResult> {
898 Ok(self.inner.list_with_delimiter(prefix).await?)
899 }
900
901 pub fn list(
902 &self,
903 path: Option<Path>,
904 ) -> Pin<Box<dyn Stream<Item = Result<ObjectMeta>> + Send>> {
905 Box::pin(ListRetryStream::new(self.inner.clone(), path, 5).map(|m| m.map_err(|e| e.into())))
906 }
907
908 pub fn read_dir_all<'a, 'b>(
912 &'a self,
913 dir_path: impl Into<&'b Path> + Send,
914 unmodified_since: Option<DateTime<Utc>>,
915 ) -> BoxStream<'a, Result<ObjectMeta>> {
916 self.inner.read_dir_all(dir_path, unmodified_since)
917 }
918
919 pub async fn remove_dir_all(&self, dir_path: impl Into<Path>) -> Result<()> {
921 let path = dir_path.into();
922 let path = Path::parse(&path)?;
923
924 if self.is_local() {
925 return super::local::remove_dir_all(&path);
927 }
928 let sub_entries = self
929 .inner
930 .list(Some(&path))
931 .map(|m| m.map(|meta| meta.location))
932 .boxed();
933 self.inner
934 .delete_stream(sub_entries)
935 .try_collect::<Vec<_>>()
936 .await?;
937 if self.scheme == "file-object-store" {
938 return super::local::remove_dir_all(&path);
941 }
942 Ok(())
943 }
944
945 pub fn remove_stream<'a>(
946 &'a self,
947 locations: BoxStream<'a, Result<Path>>,
948 ) -> BoxStream<'a, Result<Path>> {
949 let store = Arc::clone(&self.inner);
950 locations
951 .and_then(move |location| {
952 let store = Arc::clone(&store);
953 async move {
954 store.delete(&location).await?;
955 Ok(location)
956 }
957 })
958 .boxed()
959 }
960
961 pub async fn exists(&self, path: &Path) -> Result<bool> {
963 match self.inner.head(path).await {
964 Ok(_) => Ok(true),
965 Err(object_store::Error::NotFound { path: _, source: _ }) => Ok(false),
966 Err(e) => Err(e.into()),
967 }
968 }
969
970 pub async fn size(&self, path: &Path) -> Result<u64> {
972 Ok(self.inner.head(path).await?.size)
973 }
974
975 pub async fn read_one_all(&self, path: &Path) -> Result<Bytes> {
977 let reader = self.open(path).await?;
978 Ok(reader.get_all().await?)
979 }
980
981 pub async fn read_one_range(&self, path: &Path, range: Range<usize>) -> Result<Bytes> {
986 let reader = self.open(path).await?;
987 Ok(reader.get_range(range).await?)
988 }
989}
990
991#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy)]
993pub enum LanceConfigKey {
994 DownloadRetryCount,
996}
997
998impl FromStr for LanceConfigKey {
999 type Err = Error;
1000
1001 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
1002 match s.to_ascii_lowercase().as_str() {
1003 "download_retry_count" => Ok(Self::DownloadRetryCount),
1004 _ => Err(Error::invalid_input_source(
1005 format!("Invalid LanceConfigKey: {}", s).into(),
1006 )),
1007 }
1008 }
1009}
1010
1011#[derive(Clone, Debug, Default)]
1012pub struct StorageOptions(pub HashMap<String, String>);
1013
1014impl StorageOptions {
1015 pub fn new(options: HashMap<String, String>) -> Self {
1017 let mut options = options;
1018 if let Ok(value) = std::env::var("AZURE_STORAGE_ALLOW_HTTP") {
1019 options.insert("allow_http".into(), value);
1020 }
1021 if let Ok(value) = std::env::var("AZURE_STORAGE_USE_HTTP") {
1022 options.insert("allow_http".into(), value);
1023 }
1024 if let Ok(value) = std::env::var("AWS_ALLOW_HTTP") {
1025 options.insert("allow_http".into(), value);
1026 }
1027 if let Ok(value) = std::env::var("OBJECT_STORE_CLIENT_MAX_RETRIES") {
1028 options.insert("client_max_retries".into(), value);
1029 }
1030 if let Ok(value) = std::env::var("OBJECT_STORE_CLIENT_RETRY_TIMEOUT") {
1031 options.insert("client_retry_timeout".into(), value);
1032 }
1033 Self(options)
1034 }
1035
1036 pub fn allow_http(&self) -> bool {
1038 self.0.iter().any(|(key, value)| {
1039 key.to_ascii_lowercase().contains("allow_http") & str_is_truthy(value)
1040 })
1041 }
1042
1043 pub fn download_retry_count(&self) -> usize {
1045 self.0
1046 .iter()
1047 .find(|(key, _)| key.eq_ignore_ascii_case("download_retry_count"))
1048 .map(|(_, value)| value.parse::<usize>().unwrap_or(3))
1049 .unwrap_or(3)
1050 }
1051
1052 pub fn client_max_retries(&self) -> usize {
1054 self.0
1055 .iter()
1056 .find(|(key, _)| key.eq_ignore_ascii_case("client_max_retries"))
1057 .and_then(|(_, value)| value.parse::<usize>().ok())
1058 .unwrap_or(3)
1059 }
1060
1061 pub fn client_retry_timeout(&self) -> u64 {
1063 self.0
1064 .iter()
1065 .find(|(key, _)| key.eq_ignore_ascii_case("client_retry_timeout"))
1066 .and_then(|(_, value)| value.parse::<u64>().ok())
1067 .unwrap_or(180)
1068 }
1069
1070 pub fn get(&self, key: &str) -> Option<&String> {
1071 self.0.get(key)
1072 }
1073
1074 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1082 pub fn client_options(&self) -> Result<ClientOptions> {
1083 let mut headers = HeaderMap::new();
1084 for (key, value) in &self.0 {
1085 if let Some(header_name) = key.strip_prefix("headers.") {
1086 let name = header_name
1087 .parse::<http::header::HeaderName>()
1088 .map_err(|e| {
1089 Error::invalid_input(format!("invalid header name '{header_name}': {e}"))
1090 })?;
1091 let val = HeaderValue::from_str(value).map_err(|e| {
1092 Error::invalid_input(format!("invalid header value for '{header_name}': {e}"))
1093 })?;
1094 headers.insert(name, val);
1095 }
1096 }
1097 let mut client_options = ClientOptions::default();
1098 if !headers.is_empty() {
1099 client_options = client_options.with_default_headers(headers);
1100 }
1101 Ok(client_options)
1102 }
1103
1104 pub fn expires_at_millis(&self) -> Option<u64> {
1106 self.0
1107 .get(EXPIRES_AT_MILLIS_KEY)
1108 .and_then(|s| s.parse::<u64>().ok())
1109 }
1110}
1111
1112impl From<HashMap<String, String>> for StorageOptions {
1113 fn from(value: HashMap<String, String>) -> Self {
1114 Self::new(value)
1115 }
1116}
1117
1118static DEFAULT_OBJECT_STORE_REGISTRY: std::sync::LazyLock<ObjectStoreRegistry> =
1119 std::sync::LazyLock::new(ObjectStoreRegistry::default);
1120
1121impl ObjectStore {
1122 #[allow(clippy::too_many_arguments)]
1123 pub fn new(
1124 store: Arc<DynObjectStore>,
1125 location: Url,
1126 block_size: Option<usize>,
1127 wrapper: Option<Arc<dyn WrappingObjectStore>>,
1128 use_constant_size_upload_parts: bool,
1129 list_is_lexically_ordered: bool,
1130 io_parallelism: usize,
1131 download_retry_count: usize,
1132 storage_options: Option<&HashMap<String, String>>,
1133 ) -> Self {
1134 let scheme = location.scheme();
1135 let block_size = block_size.unwrap_or_else(|| infer_block_size(scheme));
1136 let store_prefix = match DEFAULT_OBJECT_STORE_REGISTRY.get_provider(scheme) {
1137 Some(provider) => provider
1138 .calculate_object_store_prefix(&location, storage_options)
1139 .unwrap(),
1140 None => {
1141 let store_prefix = format!("{}${}", location.scheme(), location.authority());
1142 log::warn!(
1143 "Guessing that object store prefix is {}, since object store scheme is not found in registry.",
1144 store_prefix
1145 );
1146 store_prefix
1147 }
1148 };
1149 let store = match wrapper {
1150 Some(wrapper) => wrapper.wrap(&store_prefix, store),
1151 None => store,
1152 };
1153
1154 let io_tracker = IOTracker::default();
1156 let tracked_store = io_tracker.wrap("", store);
1157
1158 Self {
1159 inner: tracked_store,
1160 scheme: scheme.into(),
1161 block_size,
1162 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
1163 use_constant_size_upload_parts,
1164 list_is_lexically_ordered,
1165 io_parallelism,
1166 download_retry_count,
1167 io_tracker,
1168 store_prefix,
1169 }
1170 }
1171}
1172
1173fn infer_block_size(scheme: &str) -> usize {
1174 match scheme {
1178 "file" => 4 * 1024,
1179 _ => 64 * 1024,
1180 }
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185 use super::*;
1186 use async_trait::async_trait;
1187 use bytes::Bytes;
1188 use lance_core::utils::tempfile::{TempStdDir, TempStdFile, TempStrDir};
1189 use object_store::memory::InMemory;
1190 use object_store::{
1191 CopyOptions, GetOptions, GetResult, ListResult, MultipartUpload, PutMultipartOptions,
1192 PutOptions, PutPayload, PutResult, Result as OSResult,
1193 };
1194 use rstest::rstest;
1195 use std::env::set_current_dir;
1196 use std::fmt::{Display, Formatter};
1197 use std::fs::{create_dir_all, write};
1198 use std::ops::Range;
1199 use std::path::Path as StdPath;
1200 use std::sync::atomic::{AtomicBool, Ordering};
1201
1202 fn write_to_file(path_str: &str, contents: &str) -> std::io::Result<()> {
1204 let path = expand_path(path_str).map_err(std::io::Error::other)?;
1205 std::fs::create_dir_all(path.parent().unwrap())?;
1206 write(path, contents)
1207 }
1208
1209 async fn read_from_store(store: &ObjectStore, path: &Path) -> Result<String> {
1210 let test_file_store = store.open(path).await.unwrap();
1211 let size = test_file_store.size().await.unwrap();
1212 let bytes = test_file_store.get_range(0..size).await.unwrap();
1213 let contents = String::from_utf8(bytes.to_vec()).unwrap();
1214 Ok(contents)
1215 }
1216
1217 #[test]
1218 fn test_io_parallelism_clamped_to_nonzero() {
1219 let store = ObjectStore::local();
1222
1223 unsafe { std::env::set_var("LANCE_IO_THREADS", "0") };
1226 assert_eq!(
1227 store.io_parallelism(),
1228 1,
1229 "LANCE_IO_THREADS=0 must clamp to 1"
1230 );
1231
1232 unsafe { std::env::set_var("LANCE_IO_THREADS", "8") };
1233 assert_eq!(
1234 store.io_parallelism(),
1235 8,
1236 "a positive override must pass through unchanged"
1237 );
1238
1239 unsafe { std::env::remove_var("LANCE_IO_THREADS") };
1240 assert!(
1241 store.io_parallelism() >= 1,
1242 "the configured default parallelism must be at least 1"
1243 );
1244 }
1245
1246 #[tokio::test]
1247 async fn test_absolute_paths() {
1248 let tmp_path = TempStrDir::default();
1249 write_to_file(
1250 &format!("{tmp_path}/bar/foo.lance/test_file"),
1251 "TEST_CONTENT",
1252 )
1253 .unwrap();
1254
1255 for uri in &[
1257 format!("{tmp_path}/bar/foo.lance"),
1258 format!("{tmp_path}/./bar/foo.lance"),
1259 format!("{tmp_path}/bar/foo.lance/../foo.lance"),
1260 ] {
1261 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
1262 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
1263 .await
1264 .unwrap();
1265 assert_eq!(contents, "TEST_CONTENT");
1266 }
1267 }
1268
1269 #[tokio::test]
1270 async fn test_cloud_paths() {
1271 let uri = "s3://bucket/foo.lance";
1272 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
1273 assert_eq!(store.scheme, "s3");
1274 assert_eq!(path.to_string(), "foo.lance");
1275
1276 let (store, path) = ObjectStore::from_uri("s3+ddb://bucket/foo.lance")
1277 .await
1278 .unwrap();
1279 assert_eq!(store.scheme, "s3");
1280 assert_eq!(path.to_string(), "foo.lance");
1281
1282 let (store, path) = ObjectStore::from_uri("gs://bucket/foo.lance")
1283 .await
1284 .unwrap();
1285 assert_eq!(store.scheme, "gs");
1286 assert_eq!(path.to_string(), "foo.lance");
1287
1288 let (store, path) =
1289 ObjectStore::from_uri("abfss://filesystem@account.dfs.core.windows.net/foo.lance")
1290 .await
1291 .unwrap();
1292 assert_eq!(store.scheme, "abfss");
1293 assert_eq!(path.to_string(), "foo.lance");
1294 }
1295
1296 async fn test_block_size_used_test_helper(
1297 uri: &str,
1298 storage_options: Option<HashMap<String, String>>,
1299 default_expected_block_size: usize,
1300 ) {
1301 let registry = Arc::new(ObjectStoreRegistry::default());
1303 let accessor = storage_options
1304 .clone()
1305 .map(|opts| Arc::new(StorageOptionsAccessor::with_static_options(opts)));
1306 let params = ObjectStoreParams {
1307 storage_options_accessor: accessor.clone(),
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, default_expected_block_size);
1314
1315 let registry = Arc::new(ObjectStoreRegistry::default());
1317 let params = ObjectStoreParams {
1318 block_size: Some(1024),
1319 storage_options_accessor: accessor,
1320 ..ObjectStoreParams::default()
1321 };
1322 let (store, _) = ObjectStore::from_uri_and_params(registry, uri, ¶ms)
1323 .await
1324 .unwrap();
1325 assert_eq!(store.block_size, 1024);
1326 }
1327
1328 #[rstest]
1329 #[case("s3://bucket/foo.lance", None)]
1330 #[case("gs://bucket/foo.lance", None)]
1331 #[case("az://account/bucket/foo.lance",
1332 Some(HashMap::from([
1333 (String::from("account_name"), String::from("account")),
1334 (String::from("container_name"), String::from("container"))
1335 ])))]
1336 #[case("abfss://filesystem@account.dfs.core.windows.net/foo.lance",
1337 Some(HashMap::from([
1338 (String::from("account_name"), String::from("account")),
1339 (String::from("container_name"), String::from("filesystem"))
1340 ])))]
1341 #[tokio::test]
1342 async fn test_block_size_used_cloud(
1343 #[case] uri: &str,
1344 #[case] storage_options: Option<HashMap<String, String>>,
1345 ) {
1346 test_block_size_used_test_helper(uri, storage_options, 64 * 1024).await;
1347 }
1348
1349 #[rstest]
1350 #[case("file")]
1351 #[case("file-object-store")]
1352 #[case("memory:///bucket/foo.lance")]
1353 #[tokio::test]
1354 async fn test_block_size_used_file(#[case] prefix: &str) {
1355 let tmp_path = TempStrDir::default();
1356 let path = format!("{tmp_path}/bar/foo.lance/test_file");
1357 write_to_file(&path, "URL").unwrap();
1358 let uri = format!("{prefix}:///{path}");
1359 test_block_size_used_test_helper(&uri, None, 4 * 1024).await;
1360 }
1361
1362 #[tokio::test]
1363 async fn test_relative_paths() {
1364 let tmp_path = TempStrDir::default();
1365 write_to_file(
1366 &format!("{tmp_path}/bar/foo.lance/test_file"),
1367 "RELATIVE_URL",
1368 )
1369 .unwrap();
1370
1371 set_current_dir(StdPath::new(tmp_path.as_ref())).expect("Error changing current dir");
1372 let (store, path) = ObjectStore::from_uri("./bar/foo.lance").await.unwrap();
1373
1374 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
1375 .await
1376 .unwrap();
1377 assert_eq!(contents, "RELATIVE_URL");
1378 }
1379
1380 #[tokio::test]
1381 async fn test_tilde_expansion() {
1382 let uri = "~/foo.lance";
1383 write_to_file(&format!("{uri}/test_file"), "TILDE").unwrap();
1384 let (store, path) = ObjectStore::from_uri(uri).await.unwrap();
1385 let contents = read_from_store(store.as_ref(), &path.clone().join("test_file"))
1386 .await
1387 .unwrap();
1388 assert_eq!(contents, "TILDE");
1389 }
1390
1391 #[tokio::test]
1392 async fn test_read_directory() {
1393 let path = TempStdDir::default();
1394 create_dir_all(path.join("foo").join("bar")).unwrap();
1395 create_dir_all(path.join("foo").join("zoo")).unwrap();
1396 create_dir_all(path.join("foo").join("zoo").join("abc")).unwrap();
1397 write_to_file(
1398 path.join("foo").join("test_file").to_str().unwrap(),
1399 "read_dir",
1400 )
1401 .unwrap();
1402 let (store, base) = ObjectStore::from_uri(path.to_str().unwrap()).await.unwrap();
1403
1404 let sub_dirs = store.read_dir(base.clone().join("foo")).await.unwrap();
1405 assert_eq!(sub_dirs, vec!["bar", "zoo", "test_file"]);
1406 }
1407
1408 #[tokio::test]
1409 async fn test_delete_directory_local_store() {
1410 test_delete_directory("").await;
1411 }
1412
1413 #[tokio::test]
1414 async fn test_delete_directory_file_object_store() {
1415 test_delete_directory("file-object-store").await;
1416 }
1417
1418 async fn test_delete_directory(scheme: &str) {
1419 let path = TempStdDir::default();
1420 create_dir_all(path.join("foo").join("bar")).unwrap();
1421 create_dir_all(path.join("foo").join("zoo")).unwrap();
1422 create_dir_all(path.join("foo").join("zoo").join("abc")).unwrap();
1423 write_to_file(
1424 path.join("foo")
1425 .join("bar")
1426 .join("test_file")
1427 .to_str()
1428 .unwrap(),
1429 "delete",
1430 )
1431 .unwrap();
1432 let file_url = Url::from_directory_path(&path).unwrap();
1433 let url = if scheme.is_empty() {
1434 file_url
1435 } else {
1436 let mut url = Url::parse(&format!("{scheme}:///")).unwrap();
1437 url.set_path(file_url.path());
1439 url
1440 };
1441 let (store, base) = ObjectStore::from_uri(url.as_ref()).await.unwrap();
1442 store
1443 .remove_dir_all(base.clone().join("foo"))
1444 .await
1445 .unwrap();
1446
1447 assert!(!path.join("foo").exists());
1448 }
1449
1450 #[derive(Debug)]
1451 struct TestWrapper {
1452 called: AtomicBool,
1453
1454 return_value: Arc<dyn OSObjectStore>,
1455 }
1456
1457 impl WrappingObjectStore for TestWrapper {
1458 fn wrap(
1459 &self,
1460 _store_prefix: &str,
1461 _original: Arc<dyn OSObjectStore>,
1462 ) -> Arc<dyn OSObjectStore> {
1463 self.called.store(true, Ordering::Relaxed);
1464
1465 self.return_value.clone()
1467 }
1468 }
1469
1470 impl TestWrapper {
1471 fn called(&self) -> bool {
1472 self.called.load(Ordering::Relaxed)
1473 }
1474 }
1475
1476 #[tokio::test]
1477 async fn test_wrapper_identity_is_stable_across_tasks() {
1478 let wrapper = Arc::new(TestWrapper {
1479 called: AtomicBool::new(false),
1480 return_value: Arc::new(InMemory::new()),
1481 });
1482 let initial_params = ObjectStoreParams {
1483 object_store_wrapper: Some(wrapper.clone()),
1484 ..ObjectStoreParams::default()
1485 };
1486 let task_params = tokio::spawn(async move {
1487 ObjectStoreParams {
1488 object_store_wrapper: Some(wrapper),
1489 ..ObjectStoreParams::default()
1490 }
1491 })
1492 .await
1493 .unwrap();
1494
1495 assert_eq!(initial_params, task_params);
1496
1497 let mut initial_hasher = std::hash::DefaultHasher::new();
1498 std::hash::Hash::hash(&initial_params, &mut initial_hasher);
1499 let mut task_hasher = std::hash::DefaultHasher::new();
1500 std::hash::Hash::hash(&task_params, &mut task_hasher);
1501 assert_eq!(
1502 std::hash::Hasher::finish(&initial_hasher),
1503 std::hash::Hasher::finish(&task_hasher)
1504 );
1505 }
1506
1507 #[tokio::test]
1508 async fn test_wrapping_object_store_option_is_used() {
1509 let mock_inner_store: Arc<dyn OSObjectStore> = Arc::new(InMemory::new());
1511 let registry = Arc::new(ObjectStoreRegistry::default());
1512
1513 assert_eq!(Arc::strong_count(&mock_inner_store), 1);
1514
1515 let wrapper = Arc::new(TestWrapper {
1516 called: AtomicBool::new(false),
1517 return_value: mock_inner_store.clone(),
1518 });
1519
1520 let params = ObjectStoreParams {
1521 object_store_wrapper: Some(wrapper.clone()),
1522 ..ObjectStoreParams::default()
1523 };
1524
1525 assert!(!wrapper.called());
1527
1528 let _ = ObjectStore::from_uri_and_params(registry, "memory:///", ¶ms)
1529 .await
1530 .unwrap();
1531
1532 assert!(wrapper.called());
1534
1535 assert_eq!(Arc::strong_count(&mock_inner_store), 2);
1538 }
1539
1540 #[tokio::test]
1541 async fn test_local_paths() {
1542 let file_path = TempStdFile::default();
1543 let mut writer = ObjectStore::create_local_writer(&file_path).await.unwrap();
1544 writer.write_all(b"LOCAL").await.unwrap();
1545 Writer::shutdown(&mut writer).await.unwrap();
1546
1547 let reader = ObjectStore::open_local(&file_path).await.unwrap();
1548 let buf = reader.get_range(0..5).await.unwrap();
1549 assert_eq!(buf.as_ref(), b"LOCAL");
1550 }
1551
1552 #[tokio::test]
1553 async fn test_read_one() {
1554 let file_path = TempStdFile::default();
1555 let mut writer = ObjectStore::create_local_writer(&file_path).await.unwrap();
1556 writer.write_all(b"LOCAL").await.unwrap();
1557 Writer::shutdown(&mut writer).await.unwrap();
1558
1559 let file_path_os = object_store::path::Path::parse(file_path.to_str().unwrap()).unwrap();
1560 let obj_store = ObjectStore::local();
1561 let buf = obj_store.read_one_all(&file_path_os).await.unwrap();
1562 assert_eq!(buf.as_ref(), b"LOCAL");
1563
1564 let buf = obj_store.read_one_range(&file_path_os, 0..5).await.unwrap();
1565 assert_eq!(buf.as_ref(), b"LOCAL");
1566 }
1567
1568 #[tokio::test]
1569 #[cfg(windows)]
1570 async fn test_windows_paths() {
1571 use std::path::Component;
1572 use std::path::Prefix;
1573 use std::path::Prefix::*;
1574
1575 fn get_path_prefix(path: &StdPath) -> Prefix<'_> {
1576 match path.components().next().unwrap() {
1577 Component::Prefix(prefix_component) => prefix_component.kind(),
1578 _ => panic!(),
1579 }
1580 }
1581
1582 fn get_drive_letter(prefix: Prefix) -> String {
1583 match prefix {
1584 Disk(bytes) => String::from_utf8(vec![bytes]).unwrap(),
1585 _ => panic!(),
1586 }
1587 }
1588
1589 let tmp_path = TempStdFile::default();
1590 let prefix = get_path_prefix(&tmp_path);
1591 let drive_letter = get_drive_letter(prefix);
1592
1593 write_to_file(
1594 &(format!("{drive_letter}:/test_folder/test.lance") + "/test_file"),
1595 "WINDOWS",
1596 )
1597 .unwrap();
1598
1599 for uri in &[
1600 format!("{drive_letter}:/test_folder/test.lance"),
1601 format!("{drive_letter}:\\test_folder\\test.lance"),
1602 ] {
1603 let (store, base) = ObjectStore::from_uri(uri).await.unwrap();
1604 let contents = read_from_store(store.as_ref(), &base.clone().join("test_file"))
1605 .await
1606 .unwrap();
1607 assert_eq!(contents, "WINDOWS");
1608 }
1609 }
1610
1611 #[tokio::test]
1612 async fn test_cross_filesystem_copy() {
1613 let source_dir = TempStdDir::default();
1615 let dest_dir = TempStdDir::default();
1616
1617 let source_file_name = "test_file.txt";
1619 let source_file = source_dir.join(source_file_name);
1620 std::fs::write(&source_file, b"test content").unwrap();
1621
1622 let (store, base_path) = ObjectStore::from_uri(source_dir.to_str().unwrap())
1624 .await
1625 .unwrap();
1626
1627 let from_path = base_path.clone().join(source_file_name);
1629
1630 let dest_file = dest_dir.join("copied_file.txt");
1632 let dest_str = dest_file.to_str().unwrap();
1633 let to_path = object_store::path::Path::parse(dest_str).unwrap();
1634
1635 store.copy(&from_path, &to_path).await.unwrap();
1637
1638 assert!(dest_file.exists());
1640 let copied_content = std::fs::read(&dest_file).unwrap();
1641 assert_eq!(copied_content, b"test content");
1642 }
1643
1644 #[tokio::test]
1645 async fn test_copy_creates_parent_directories() {
1646 let source_dir = TempStdDir::default();
1647 let dest_dir = TempStdDir::default();
1648
1649 let source_file_name = "test_file.txt";
1651 let source_file = source_dir.join(source_file_name);
1652 std::fs::write(&source_file, b"test content").unwrap();
1653
1654 let (store, base_path) = ObjectStore::from_uri(source_dir.to_str().unwrap())
1656 .await
1657 .unwrap();
1658
1659 let from_path = base_path.clone().join(source_file_name);
1661
1662 let dest_file = dest_dir.join("nested").join("dirs").join("copied_file.txt");
1664 let dest_str = dest_file.to_str().unwrap();
1665 let to_path = object_store::path::Path::parse(dest_str).unwrap();
1666
1667 store.copy(&from_path, &to_path).await.unwrap();
1669
1670 assert!(dest_file.exists());
1672 assert!(dest_file.parent().unwrap().exists());
1673 let copied_content = std::fs::read(&dest_file).unwrap();
1674 assert_eq!(copied_content, b"test content");
1675 }
1676
1677 #[derive(Debug)]
1682 struct CopyFailingStore {
1683 inner: InMemory,
1684 }
1685
1686 impl Display for CopyFailingStore {
1687 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1688 write!(f, "CopyFailingStore")
1689 }
1690 }
1691
1692 #[async_trait]
1693 impl OSObjectStore for CopyFailingStore {
1694 async fn put_opts(
1695 &self,
1696 location: &Path,
1697 bytes: PutPayload,
1698 opts: PutOptions,
1699 ) -> OSResult<PutResult> {
1700 self.inner.put_opts(location, bytes, opts).await
1701 }
1702 async fn put_multipart_opts(
1703 &self,
1704 location: &Path,
1705 opts: PutMultipartOptions,
1706 ) -> OSResult<Box<dyn MultipartUpload>> {
1707 self.inner.put_multipart_opts(location, opts).await
1708 }
1709 async fn get_opts(&self, location: &Path, options: GetOptions) -> OSResult<GetResult> {
1710 self.inner.get_opts(location, options).await
1711 }
1712 async fn get_ranges(&self, location: &Path, ranges: &[Range<u64>]) -> OSResult<Vec<Bytes>> {
1713 self.inner.get_ranges(location, ranges).await
1714 }
1715 fn delete_stream(
1716 &self,
1717 locations: BoxStream<'static, OSResult<Path>>,
1718 ) -> BoxStream<'static, OSResult<Path>> {
1719 self.inner.delete_stream(locations)
1720 }
1721 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, OSResult<ObjectMeta>> {
1722 self.inner.list(prefix)
1723 }
1724 fn list_with_offset(
1725 &self,
1726 prefix: Option<&Path>,
1727 offset: &Path,
1728 ) -> BoxStream<'static, OSResult<ObjectMeta>> {
1729 self.inner.list_with_offset(prefix, offset)
1730 }
1731 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> OSResult<ListResult> {
1732 self.inner.list_with_delimiter(prefix).await
1733 }
1734 async fn copy_opts(&self, _from: &Path, _to: &Path, _opts: CopyOptions) -> OSResult<()> {
1735 Err(object_store::Error::Generic {
1736 store: "CopyFailingStore",
1737 source: "single-shot copy disabled in test".into(),
1738 })
1739 }
1740 }
1741
1742 #[tokio::test]
1743 async fn test_copy_streams_objects_larger_than_threshold() {
1744 let mut store = ObjectStore::memory();
1750 store.inner = Arc::new(CopyFailingStore {
1751 inner: InMemory::new(),
1752 });
1753
1754 let from = Path::from("source.bin");
1755 let contents = b"streaming multipart copy payload well past the tiny threshold";
1756 store.put(&from, contents).await.unwrap();
1757
1758 let streamed = Path::from("streamed.bin");
1761 store.copy_impl(&from, &streamed, true, 8).await.unwrap();
1762 let copied = store.read_one_all(&streamed).await.unwrap();
1763 assert_eq!(copied.as_ref(), contents.as_slice());
1764
1765 let native = Path::from("native.bin");
1769 assert!(
1770 store
1771 .copy_impl(&from, &native, true, u64::MAX)
1772 .await
1773 .is_err()
1774 );
1775 }
1776
1777 #[test]
1778 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1779 fn test_client_options_extracts_headers() {
1780 let opts = StorageOptions(HashMap::from([
1781 ("headers.x-custom-foo".to_string(), "bar".to_string()),
1782 ("headers.x-ms-version".to_string(), "2023-11-03".to_string()),
1783 ("region".to_string(), "us-west-2".to_string()),
1784 ]));
1785 let client_options = opts.client_options().unwrap();
1786
1787 let opts_no_headers = StorageOptions(HashMap::from([(
1790 "region".to_string(),
1791 "us-west-2".to_string(),
1792 )]));
1793 opts_no_headers.client_options().unwrap();
1794
1795 #[cfg(feature = "gcp")]
1799 {
1800 use object_store::gcp::GoogleCloudStorageBuilder;
1801 let _builder = GoogleCloudStorageBuilder::new()
1802 .with_client_options(client_options)
1803 .with_url("gs://test-bucket");
1804 }
1805 }
1806
1807 #[test]
1808 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1809 fn test_client_options_rejects_invalid_header_name() {
1810 let opts = StorageOptions(HashMap::from([(
1811 "headers.bad header".to_string(),
1812 "value".to_string(),
1813 )]));
1814 let err = opts.client_options().unwrap_err();
1815 assert!(err.to_string().contains("invalid header name"));
1816 }
1817
1818 #[test]
1819 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1820 fn test_client_options_rejects_invalid_header_value() {
1821 let opts = StorageOptions(HashMap::from([(
1822 "headers.x-good-name".to_string(),
1823 "bad\x01value".to_string(),
1824 )]));
1825 let err = opts.client_options().unwrap_err();
1826 assert!(err.to_string().contains("invalid header value"));
1827 }
1828
1829 #[test]
1830 #[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))]
1831 fn test_client_options_empty_when_no_header_keys() {
1832 let opts = StorageOptions(HashMap::from([
1833 ("region".to_string(), "us-east-1".to_string()),
1834 ("access_key_id".to_string(), "AKID".to_string()),
1835 ]));
1836 opts.client_options().unwrap();
1837 }
1838}