pub mod local;
use std::fmt::Debug;
use std::io::Read;
use std::path;
use std::path::{Component, Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use async_trait::async_trait;
use futures::future::ready;
use futures::{AsyncRead, Stream, StreamExt, TryStreamExt};
use glob::Pattern;
use crate::{FileMeta, ListEntry, Result, SizedFile};
pub type FileMetaStream =
Pin<Box<dyn Stream<Item = Result<FileMeta>> + Send + Sync + 'static>>;
pub type ListEntryStream =
Pin<Box<dyn Stream<Item = Result<ListEntry>> + Send + Sync + 'static>>;
pub type ObjectReaderStream =
Pin<Box<dyn Stream<Item = Result<Arc<dyn ObjectReader>>> + Send + Sync>>;
#[async_trait]
pub trait ObjectReader: Send + Sync {
async fn chunk_reader(&self, start: u64, length: usize)
-> Result<Box<dyn AsyncRead>>;
fn sync_chunk_reader(
&self,
start: u64,
length: usize,
) -> Result<Box<dyn Read + Send + Sync>>;
fn sync_reader(&self) -> Result<Box<dyn Read + Send + Sync>> {
self.sync_chunk_reader(0, self.length() as usize)
}
fn length(&self) -> u64;
}
#[async_trait]
pub trait ObjectStore: Sync + Send + Debug {
async fn list_file(&self, prefix: &str) -> Result<FileMetaStream>;
async fn list_file_with_suffix(
&self,
prefix: &str,
suffix: &str,
) -> Result<FileMetaStream> {
self.glob_file_with_suffix(prefix, suffix).await
}
async fn glob_file(&self, glob_pattern: &str) -> Result<FileMetaStream> {
if !contains_glob_start_char(glob_pattern) {
self.list_file(glob_pattern).await
} else {
let normalized_glob_pb = normalize_path(Path::new(glob_pattern));
let normalized_glob_pattern =
normalized_glob_pb.as_os_str().to_str().unwrap();
let start_path =
find_longest_search_path_without_glob_pattern(normalized_glob_pattern);
let file_stream = self.list_file(&start_path).await?;
let pattern = Pattern::new(normalized_glob_pattern).unwrap();
Ok(Box::pin(file_stream.filter(move |fr| {
let matches_pattern = match fr {
Ok(f) => pattern.matches(f.path()),
Err(_) => true,
};
async move { matches_pattern }
})))
}
}
async fn glob_file_with_suffix(
&self,
glob_pattern: &str,
suffix: &str,
) -> Result<FileMetaStream> {
let files_to_consider = match contains_glob_start_char(glob_pattern) {
true => self.glob_file(glob_pattern).await,
false => self.list_file(glob_pattern).await,
}?;
match suffix.is_empty() {
true => Ok(files_to_consider),
false => filter_suffix(files_to_consider, suffix),
}
}
async fn list_dir(
&self,
prefix: &str,
delimiter: Option<String>,
) -> Result<ListEntryStream>;
fn file_reader(&self, file: SizedFile) -> Result<Arc<dyn ObjectReader>>;
}
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
let ends_with_slash = path
.as_ref()
.to_str()
.map_or(false, |s| s.ends_with(path::MAIN_SEPARATOR));
let mut normalized = PathBuf::new();
for component in path.as_ref().components() {
match &component {
Component::ParentDir => {
if !normalized.pop() {
normalized.push(component);
}
}
_ => {
normalized.push(component);
}
}
}
if ends_with_slash {
normalized.push("");
}
normalized
}
const GLOB_START_CHARS: [char; 3] = ['?', '*', '['];
fn contains_glob_start_char(path: &str) -> bool {
path.chars().any(|c| GLOB_START_CHARS.contains(&c))
}
fn filter_suffix(file_stream: FileMetaStream, suffix: &str) -> Result<FileMetaStream> {
let suffix = suffix.to_owned();
Ok(Box::pin(
file_stream.try_filter(move |f| ready(f.path().ends_with(&suffix))),
))
}
fn find_longest_search_path_without_glob_pattern(glob_pattern: &str) -> String {
if !contains_glob_start_char(glob_pattern) {
glob_pattern.to_string()
} else {
let components_in_glob_pattern = Path::new(glob_pattern).components();
let mut path_buf_for_longest_search_path_without_glob_pattern = PathBuf::new();
for component_in_glob_pattern in components_in_glob_pattern {
let component_as_str =
component_in_glob_pattern.as_os_str().to_str().unwrap();
if contains_glob_start_char(component_as_str) {
break;
}
path_buf_for_longest_search_path_without_glob_pattern
.push(component_in_glob_pattern);
}
let mut result = path_buf_for_longest_search_path_without_glob_pattern
.to_str()
.unwrap()
.to_string();
if path_buf_for_longest_search_path_without_glob_pattern
.components()
.count()
> 1
{
result.push(path::MAIN_SEPARATOR);
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_is_glob_path() -> Result<()> {
assert!(!contains_glob_start_char("/"));
assert!(!contains_glob_start_char("/test"));
assert!(!contains_glob_start_char("/test/"));
assert!(contains_glob_start_char("/test*"));
Ok(())
}
fn test_longest_base_path(input: &str, expected: &str) {
assert_eq!(
find_longest_search_path_without_glob_pattern(input),
expected,
"testing find_longest_search_path_without_glob_pattern with {}",
input
);
}
#[tokio::test]
async fn test_find_longest_search_path_without_glob_pattern() -> Result<()> {
test_longest_base_path("/", "/");
test_longest_base_path("/a.txt", "/a.txt");
test_longest_base_path("/a", "/a");
test_longest_base_path("/a/", "/a/");
test_longest_base_path("/a/b", "/a/b");
test_longest_base_path("/a/b/", "/a/b/");
test_longest_base_path("/a/b.txt", "/a/b.txt");
test_longest_base_path("/a/b/c.txt", "/a/b/c.txt");
use path::MAIN_SEPARATOR;
test_longest_base_path("/*.txt", &format!("{MAIN_SEPARATOR}"));
test_longest_base_path(
"/a/*b.txt",
&format!("{MAIN_SEPARATOR}a{MAIN_SEPARATOR}"),
);
test_longest_base_path(
"/a/*/b.txt",
&format!("{MAIN_SEPARATOR}a{MAIN_SEPARATOR}"),
);
test_longest_base_path(
"/a/b/[123]/file*.txt",
&format!("{MAIN_SEPARATOR}a{MAIN_SEPARATOR}b{MAIN_SEPARATOR}"),
);
test_longest_base_path(
"/a/b*.txt",
&format!("{MAIN_SEPARATOR}a{MAIN_SEPARATOR}"),
);
test_longest_base_path(
"/a/b/**/c*.txt",
&format!("{MAIN_SEPARATOR}a{MAIN_SEPARATOR}b{MAIN_SEPARATOR}"),
);
test_longest_base_path(
&format!("{}/alltypes_plain*.parquet", "/a/b/c//"), &format!(
"{MAIN_SEPARATOR}a{MAIN_SEPARATOR}b{MAIN_SEPARATOR}c{MAIN_SEPARATOR}"
),
);
Ok(())
}
}