1mod session;
7
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use futures::TryStreamExt;
12use session::MultiFileSessionExt;
13use tracing::debug;
14use vortex_error::VortexResult;
15use vortex_error::vortex_bail;
16use vortex_io::filesystem::FileListing;
17use vortex_io::filesystem::FileSystemRef;
18use vortex_layout::LayoutReaderRef;
19use vortex_layout::scan::multi::LayoutReaderFactory;
20use vortex_layout::scan::multi::MultiLayoutDataSource;
21use vortex_scan::DataSource;
22use vortex_session::VortexSession;
23
24use crate::OpenOptionsSessionExt;
25use crate::VortexFile;
26use crate::VortexOpenOptions;
27
28pub struct MultiFileDataSource {
59 session: VortexSession,
60 glob_sources: Vec<(String, Option<FileSystemRef>)>,
63 open_options_fn: Arc<dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync>,
64}
65
66impl MultiFileDataSource {
67 pub fn new(session: VortexSession) -> Self {
69 Self {
70 session,
71 glob_sources: Vec::new(),
72 open_options_fn: Arc::new(|opts| opts),
73 }
74 }
75
76 pub fn with_glob(mut self, glob: impl Into<String>, fs: Option<FileSystemRef>) -> Self {
83 let glob = glob.into();
84 let glob = if fs.is_none() && std::path::Path::new(&glob).is_relative() {
85 std::env::current_dir()
86 .map(|cwd| cwd.join(&glob).to_string_lossy().into_owned())
87 .unwrap_or(glob)
88 .trim_start_matches('/')
89 .to_string()
90 } else {
91 glob.trim_start_matches('/').to_string()
92 };
93 self.glob_sources.push((glob, fs));
94 self
95 }
96
97 pub fn with_open_options(
101 mut self,
102 f: impl Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync + 'static,
103 ) -> Self {
104 self.open_options_fn = Arc::new(f);
105 self
106 }
107
108 pub async fn build(self) -> VortexResult<MultiLayoutDataSource> {
113 if self.glob_sources.is_empty() {
114 vortex_bail!("MultiFileDataSource requires at least one glob pattern");
115 }
116
117 let local_fs: Option<FileSystemRef> = self
119 .glob_sources
120 .iter()
121 .any(|(_, fs)| fs.is_none())
122 .then(|| create_local_filesystem(&self.session))
123 .transpose()?;
124
125 let mut all_files: Vec<(FileListing, FileSystemRef)> = Vec::new();
127 for (glob, maybe_fs) in &self.glob_sources {
128 let fs = maybe_fs
131 .as_ref()
132 .or(local_fs.as_ref())
133 .map(Arc::clone)
134 .unwrap_or_else(|| {
135 unreachable!("local_fs is set when any glob lacks a filesystem")
136 });
137 let files: Vec<FileListing> = fs.glob(glob)?.try_collect().await?;
138 for file in files {
139 all_files.push((file, Arc::clone(&fs)));
140 }
141 }
142
143 if all_files.is_empty() {
144 let globs: Vec<_> = self.glob_sources.iter().map(|(g, _)| g.as_str()).collect();
145 vortex_bail!("No files matched the glob pattern(s): {:?}", globs);
146 }
147
148 let file_count = all_files.len();
149 let globs: Vec<_> = self.glob_sources.iter().map(|(g, _)| g.as_str()).collect();
150 debug!(file_count, glob = ?globs, "discovered files");
151
152 let (first_file_listing, first_fs) = &all_files[0];
154 let open_fn = self.open_options_fn.as_ref();
155 let first_file = open_file(first_fs, first_file_listing, &self.session, open_fn).await?;
156 let first_reader = first_file.layout_reader()?;
157
158 let factories: Vec<Arc<dyn LayoutReaderFactory>> = all_files[1..]
159 .iter()
160 .map(|(file, fs)| {
161 Arc::new(VortexFileReaderFactory {
162 fs: Arc::clone(fs),
163 file: file.clone(),
164 session: self.session.clone(),
165 open_options_fn: Arc::clone(&self.open_options_fn),
166 }) as Arc<dyn LayoutReaderFactory>
167 })
168 .collect();
169
170 let inner = MultiLayoutDataSource::new_with_first(first_reader, factories, &self.session);
171
172 debug!(file_count, dtype = %inner.dtype(), "built MultiFileDataSource");
173
174 Ok(inner)
175 }
176}
177
178#[cfg(feature = "object_store")]
183fn create_local_filesystem(session: &VortexSession) -> VortexResult<FileSystemRef> {
184 use vortex_io::object_store::ObjectStoreFileSystem;
185 use vortex_io::session::RuntimeSessionExt;
186
187 let store = Arc::new(object_store::local::LocalFileSystem::default());
188 let fs: FileSystemRef = Arc::new(ObjectStoreFileSystem::new(store, session.handle()));
189 Ok(fs)
190}
191
192#[cfg(not(feature = "object_store"))]
193fn create_local_filesystem(_session: &VortexSession) -> VortexResult<FileSystemRef> {
194 vortex_bail!(
195 "The 'object_store' feature is required for automatic local filesystem creation. \
196 Either enable the feature or provide a filesystem via .with_filesystem()."
197 );
198}
199
200async fn open_file(
202 fs: &FileSystemRef,
203 file: &FileListing,
204 session: &VortexSession,
205 open_options_fn: &(dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync),
206) -> VortexResult<VortexFile> {
207 tracing::trace!(path = %file.path, "opening vortex file");
208
209 let source = fs.open_read(&file.path).await?;
213 let cache_key = source
214 .uri()
215 .map(|u| u.to_string())
216 .unwrap_or_else(|| file.path.clone());
217
218 let options = {
221 let mut options = open_options_fn(session.open_options());
222 if let Some(size) = file.size {
223 options = options.with_file_size(size);
224 }
225 if let Some(footer) = session.multi_file().get_footer(&cache_key) {
226 options = options.with_footer(footer);
227 }
228 options
229 };
230
231 let vortex_file = options.open(source).await?;
232
233 session
235 .multi_file()
236 .put_footer(&cache_key, vortex_file.footer().clone());
237 Ok(vortex_file)
238}
239
240struct VortexFileReaderFactory {
242 fs: FileSystemRef,
243 file: FileListing,
244 session: VortexSession,
245 open_options_fn: Arc<dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync>,
246}
247
248#[async_trait]
249impl LayoutReaderFactory for VortexFileReaderFactory {
250 async fn open(&self) -> VortexResult<Option<LayoutReaderRef>> {
251 let file = open_file(
252 &self.fs,
253 &self.file,
254 &self.session,
255 self.open_options_fn.as_ref(),
256 )
257 .await?;
258
259 Ok(Some(file.layout_reader()?))
260 }
261}