1mod session;
7mod uri;
8
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use futures::StreamExt;
13use futures::TryStreamExt;
14use futures::stream;
15pub use session::MultiFileSession;
16use session::MultiFileSessionExt;
17use tracing::debug;
18pub use uri::parse_uri_or_path;
19use vortex_error::VortexError;
20use vortex_error::VortexResult;
21use vortex_error::vortex_bail;
22use vortex_io::filesystem::FileListing;
23use vortex_io::filesystem::FileSystemRef;
24use vortex_layout::LayoutReaderRef;
25use vortex_layout::scan::multi::LayoutReaderFactory;
26use vortex_layout::scan::multi::MultiLayoutDataSource;
27use vortex_scan::DataSource;
28use vortex_session::VortexSession;
29
30use crate::OpenOptionsSessionExt;
31use crate::VortexFile;
32use crate::VortexOpenOptions;
33
34pub struct MultiFileDataSource {
65 session: VortexSession,
66 glob_sources: Vec<(String, Option<FileSystemRef>)>,
69 open_options_fn: Arc<dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync>,
70}
71
72const GLOB_RESOLUTION_CONCURRENCY: usize = 16;
76
77impl MultiFileDataSource {
78 pub fn new(session: VortexSession) -> Self {
80 Self {
81 session,
82 glob_sources: Vec::new(),
83 open_options_fn: Arc::new(|opts| opts),
84 }
85 }
86
87 pub fn with_glob(mut self, glob: impl Into<String>, fs: Option<FileSystemRef>) -> Self {
94 let glob = glob.into();
95 let glob = if fs.is_none() && std::path::Path::new(&glob).is_relative() {
96 std::env::current_dir()
97 .map(|cwd| cwd.join(&glob).to_string_lossy().into_owned())
98 .unwrap_or(glob)
99 .trim_start_matches('/')
100 .to_string()
101 } else {
102 glob.trim_start_matches('/').to_string()
103 };
104 self.glob_sources.push((glob, fs));
105 self
106 }
107
108 pub fn with_open_options(
112 mut self,
113 f: impl Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync + 'static,
114 ) -> Self {
115 self.open_options_fn = Arc::new(f);
116 self
117 }
118
119 pub async fn build(self) -> VortexResult<MultiLayoutDataSource> {
124 if self.glob_sources.is_empty() {
125 vortex_bail!("MultiFileDataSource requires at least one glob pattern");
126 }
127
128 let local_fs: Option<FileSystemRef> = self
130 .glob_sources
131 .iter()
132 .any(|(_, fs)| fs.is_none())
133 .then(|| create_local_filesystem(&self.session))
134 .transpose()?;
135
136 let globs: Vec<String> = self.glob_sources.iter().map(|(g, _)| g.clone()).collect();
137
138 let resolved: Vec<Vec<(FileListing, FileSystemRef)>> =
141 stream::iter(self.glob_sources.into_iter().map(|(glob, maybe_fs)| {
142 let fs = maybe_fs
145 .or_else(|| local_fs.as_ref().map(Arc::clone))
146 .unwrap_or_else(|| {
147 unreachable!("local_fs is set when any glob lacks a filesystem")
148 });
149 async move {
150 let files: Vec<FileListing> = fs.glob(&glob)?.try_collect().await?;
151 Ok::<_, VortexError>(
152 files
153 .into_iter()
154 .map(|file| (file, Arc::clone(&fs)))
155 .collect(),
156 )
157 }
158 }))
159 .buffered(GLOB_RESOLUTION_CONCURRENCY)
160 .try_collect()
161 .await?;
162 let all_files: Vec<(FileListing, FileSystemRef)> = resolved.into_iter().flatten().collect();
163
164 if all_files.is_empty() {
165 vortex_bail!("No files matched the glob pattern(s): {:?}", globs);
166 }
167
168 let file_count = all_files.len();
169 debug!(file_count, glob = ?globs, "discovered files");
170
171 let (first_file_listing, first_fs) = &all_files[0];
173 let open_fn = self.open_options_fn.as_ref();
174 let first_file = open_file(first_fs, first_file_listing, &self.session, open_fn).await?;
175 let first_reader = first_file.layout_reader()?;
176
177 let byte_sizes: Vec<Option<u64>> = all_files.iter().map(|(file, _)| file.size).collect();
178
179 let factories: Vec<Arc<dyn LayoutReaderFactory>> = all_files[1..]
180 .iter()
181 .map(|(file, fs)| {
182 Arc::new(VortexFileReaderFactory {
183 fs: Arc::clone(fs),
184 file: file.clone(),
185 session: self.session.clone(),
186 open_options_fn: Arc::clone(&self.open_options_fn),
187 }) as Arc<dyn LayoutReaderFactory>
188 })
189 .collect();
190
191 let inner = MultiLayoutDataSource::new_with_first(
192 first_reader,
193 factories,
194 byte_sizes,
195 &self.session,
196 );
197
198 debug!(file_count, dtype = %inner.dtype(), "built MultiFileDataSource");
199
200 Ok(inner)
201 }
202}
203
204#[cfg(feature = "object_store")]
209fn create_local_filesystem(session: &VortexSession) -> VortexResult<FileSystemRef> {
210 use vortex_io::object_store::ObjectStoreFileSystem;
211 use vortex_io::session::RuntimeSessionExt;
212
213 let store = Arc::new(object_store::local::LocalFileSystem::default());
214 let fs: FileSystemRef = Arc::new(ObjectStoreFileSystem::new(store, session.handle()));
215 Ok(fs)
216}
217
218#[cfg(not(feature = "object_store"))]
219fn create_local_filesystem(_session: &VortexSession) -> VortexResult<FileSystemRef> {
220 vortex_bail!(
221 "The 'object_store' feature is required for automatic local filesystem creation. \
222 Either enable the feature or provide a filesystem via .with_filesystem()."
223 );
224}
225
226async fn open_file(
228 fs: &FileSystemRef,
229 file: &FileListing,
230 session: &VortexSession,
231 open_options_fn: &(dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync),
232) -> VortexResult<VortexFile> {
233 tracing::trace!(path = %file.path, "opening vortex file");
234
235 let source = fs.open_read(&file.path).await?;
239 let cache_key = source
240 .uri()
241 .map(|u| u.to_string())
242 .unwrap_or_else(|| file.path.clone());
243
244 let options = {
247 let mut options = open_options_fn(session.open_options());
248 if let Some(size) = file.size {
249 options = options.with_file_size(size);
250 }
251 if let Some(footer) = session.multi_file().get_footer(&cache_key) {
252 options = options.with_footer(footer);
253 }
254 options
255 };
256
257 let vortex_file = options.open(source).await?;
258
259 session
261 .multi_file()
262 .put_footer(&cache_key, vortex_file.footer().clone());
263 Ok(vortex_file)
264}
265
266struct VortexFileReaderFactory {
268 fs: FileSystemRef,
269 file: FileListing,
270 session: VortexSession,
271 open_options_fn: Arc<dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync>,
272}
273
274#[async_trait]
275impl LayoutReaderFactory for VortexFileReaderFactory {
276 async fn open(&self) -> VortexResult<Option<LayoutReaderRef>> {
277 let file = open_file(
278 &self.fs,
279 &self.file,
280 &self.session,
281 self.open_options_fn.as_ref(),
282 )
283 .await?;
284
285 Ok(Some(file.layout_reader()?))
286 }
287}