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