mod session;
use std::sync::Arc;
use async_trait::async_trait;
use futures::TryStreamExt;
use session::MultiFileSessionExt;
use tracing::debug;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_error::vortex_err;
use vortex_io::filesystem::FileListing;
use vortex_io::filesystem::FileSystemRef;
use vortex_layout::LayoutReaderRef;
use vortex_layout::scan::multi::LayoutReaderFactory;
use vortex_layout::scan::multi::MultiLayoutDataSource;
use vortex_scan::DataSource;
use vortex_session::VortexSession;
use crate::OpenOptionsSessionExt;
use crate::VortexOpenOptions;
use crate::v2::FileStatsLayoutReader;
pub struct MultiFileDataSource {
session: VortexSession,
fs: Option<FileSystemRef>,
glob: Option<String>,
open_options_fn: Arc<dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync>,
}
impl MultiFileDataSource {
pub fn new(session: VortexSession) -> Self {
Self {
session,
fs: None,
glob: None,
open_options_fn: Arc::new(|opts| opts),
}
}
pub fn with_glob(mut self, glob: impl Into<String>) -> Self {
self.glob = Some(glob.into().trim_start_matches("/").to_string());
self
}
pub fn with_filesystem(mut self, fs: FileSystemRef) -> Self {
self.fs = Some(fs);
self
}
pub fn with_open_options(
mut self,
f: impl Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync + 'static,
) -> Self {
self.open_options_fn = Arc::new(f);
self
}
pub async fn build(mut self) -> VortexResult<impl DataSource> {
let glob = self
.glob
.take()
.ok_or_else(|| vortex_err!("MultiFileDataSource requires a glob URL"))?;
let fs = match self.fs {
Some(fs) => fs,
None => create_local_filesystem(&self.session)?,
};
let files: Vec<FileListing> = fs.glob(&glob)?.try_collect().await?;
if files.is_empty() {
vortex_bail!("No files matched the glob pattern '{}'", glob);
}
let file_count = files.len();
debug!(file_count, glob = %glob, "discovered files");
let first_file =
open_file(&fs, &files[0], &self.session, self.open_options_fn.as_ref()).await?;
let first_reader = layout_reader_with_stats(&first_file)?;
let factories: Vec<Arc<dyn LayoutReaderFactory>> = files[1..]
.iter()
.map(|f| {
Arc::new(VortexFileReaderFactory {
fs: fs.clone(),
file: f.clone(),
session: self.session.clone(),
open_options_fn: self.open_options_fn.clone(),
}) as Arc<dyn LayoutReaderFactory>
})
.collect();
let inner = MultiLayoutDataSource::new_with_first(first_reader, factories, &self.session);
debug!(file_count, dtype = %inner.dtype(), "built MultiFileDataSource");
Ok(inner)
}
}
#[cfg(feature = "object_store")]
fn create_local_filesystem(session: &VortexSession) -> VortexResult<FileSystemRef> {
use vortex_io::object_store::ObjectStoreFileSystem;
use vortex_io::session::RuntimeSessionExt;
let store = Arc::new(object_store::local::LocalFileSystem::default());
let fs: FileSystemRef = Arc::new(ObjectStoreFileSystem::new(store, session.handle()));
Ok(fs)
}
#[cfg(not(feature = "object_store"))]
fn create_local_filesystem(_session: &VortexSession) -> VortexResult<FileSystemRef> {
vortex_bail!(
"The 'object_store' feature is required for automatic local filesystem creation. \
Either enable the feature or provide a filesystem via .with_filesystem()."
);
}
async fn open_file(
fs: &FileSystemRef,
file: &FileListing,
session: &VortexSession,
open_options_fn: &(dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync),
) -> VortexResult<crate::VortexFile> {
debug!(path = %file.path, "opening vortex file");
let source = fs.open_read(&file.path).await?;
let cache_key = source
.uri()
.map(|u| u.to_string())
.unwrap_or_else(|| file.path.clone());
let options = {
let mut options = open_options_fn(session.open_options());
if let Some(size) = file.size {
options = options.with_file_size(size);
}
if let Some(footer) = session.multi_file().get_footer(&cache_key) {
options = options.with_footer(footer);
}
options
};
let vortex_file = options.open(source).await?;
session
.multi_file()
.put_footer(&cache_key, vortex_file.footer().clone());
Ok(vortex_file)
}
fn layout_reader_with_stats(file: &crate::VortexFile) -> VortexResult<LayoutReaderRef> {
let mut reader = file.layout_reader()?;
if let Some(stats) = file.file_stats().cloned() {
reader = Arc::new(FileStatsLayoutReader::new(
reader,
stats,
file.session.clone(),
));
}
Ok(reader)
}
struct VortexFileReaderFactory {
fs: FileSystemRef,
file: FileListing,
session: VortexSession,
open_options_fn: Arc<dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync>,
}
#[async_trait]
impl LayoutReaderFactory for VortexFileReaderFactory {
async fn open(&self) -> VortexResult<Option<LayoutReaderRef>> {
let file = open_file(
&self.fs,
&self.file,
&self.session,
self.open_options_fn.as_ref(),
)
.await?;
Ok(Some(layout_reader_with_stats(&file)?))
}
}