use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use parking_lot::Mutex;
use crate::common::mmap::AdviceSetting;
use crate::common::universal_io::traits::CachedReadFs;
use crate::common::universal_io::{
ListedFile, OpenExtra, OpenOptions, Populate, UioResult, UniversalIoError,
UniversalReadFileOps, UniversalReadFs,
};
#[derive(Clone, Debug)]
pub struct FileInfo {
pub size: u64,
pub last_modified: Option<std::time::SystemTime>,
}
pub struct CachedFs<Fs: UniversalReadFs> {
fs: Fs,
prefix_path: PathBuf,
files_info: Option<HashMap<PathBuf, FileInfo>>,
files_prefetched: Arc<Mutex<HashMap<PathBuf, Fs::File>>>,
}
impl<Fs: UniversalReadFs> Clone for CachedFs<Fs> {
fn clone(&self) -> Self {
let Self {
fs,
prefix_path,
files_info,
files_prefetched,
} = self;
Self {
fs: fs.clone(),
prefix_path: prefix_path.clone(),
files_info: files_info.clone(),
files_prefetched: files_prefetched.clone(),
}
}
}
impl<Fs: UniversalReadFs> CachedFs<Fs> {
pub fn new(fs: Fs, prefix_path: &Path) -> UioResult<Self> {
Ok(Self {
fs,
prefix_path: prefix_path.to_path_buf(),
files_info: None,
files_prefetched: Arc::new(Mutex::new(HashMap::new())),
})
}
pub fn inner(&self) -> &Fs {
&self.fs
}
pub fn file_info(&self, path: &Path) -> Option<&FileInfo> {
self.files_info.as_ref()?.get(path)
}
fn cached_list_files(&self, prefix_path: &Path) -> Vec<ListedFile> {
let dir = prefix_path.parent().unwrap_or(Path::new(""));
let name_prefix = prefix_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
self.files_info
.iter()
.flatten()
.filter(|(path, _)| {
path.strip_prefix(dir)
.ok()
.and_then(|rel| rel.components().next())
.is_some_and(|first| {
first
.as_os_str()
.to_string_lossy()
.starts_with(&name_prefix)
})
})
.map(|(path, info)| ListedFile {
path: path.clone(),
size: info.size,
last_modified: info.last_modified,
})
.collect()
}
}
impl<Fs: UniversalReadFs> CachedReadFs for CachedFs<Fs> {
fn cache_file_info(&mut self) -> UioResult<()> {
let list = self.fs.list_files(&self.prefix_path)?;
let files_info: HashMap<_, _> = list
.into_iter()
.map(
|ListedFile {
path,
size,
last_modified,
}| {
let info = FileInfo {
size,
last_modified,
};
(path, info)
},
)
.collect();
self.files_info = Some(files_info);
Ok(())
}
fn schedule_prefetch(
&self,
path: &Path,
open_arguments: Option<OpenOptions>,
open_extra: Option<Fs::OpenExtra>,
) -> UioResult<()> {
let mut files_prefetched = self.files_prefetched.lock();
if files_prefetched.contains_key(path) {
return Ok(());
}
let open_options = open_arguments.unwrap_or(OpenOptions {
writeable: false,
need_sequential: false,
populate: Populate::PreferBackground,
advice: AdviceSetting::Global,
});
let mut open_extra = open_extra.unwrap_or_default();
if let Some(info) = self.file_info(path) {
open_extra = open_extra.with_known_len(info.size);
}
let file = self.fs.open(path, open_options, open_extra)?;
files_prefetched.insert(path.to_path_buf(), file);
Ok(())
}
fn cached_file_info(&self, path: &Path) -> Option<FileInfo> {
self.files_info.as_ref()?.get(path).cloned()
}
}
pub struct CachedReadFsContext<C> {
pub inner: C,
pub prefix_path: PathBuf,
}
impl<Fs: UniversalReadFs> UniversalReadFileOps for CachedFs<Fs> {
type ContextConfig = CachedReadFsContext<Fs::ContextConfig>;
fn from_context(context: Self::ContextConfig) -> UioResult<Self> {
let CachedReadFsContext { inner, prefix_path } = context;
Self::new(Fs::from_context(inner)?, &prefix_path)
}
fn list_files(&self, prefix_path: &Path) -> UioResult<Vec<ListedFile>> {
match &self.files_info {
Some(_) => Ok(self.cached_list_files(prefix_path)),
None => self.fs.list_files(prefix_path),
}
}
fn exists(&self, path: &Path) -> UioResult<bool> {
match &self.files_info {
Some(files_info) => Ok(files_info.contains_key(path)),
None => self.fs.exists(path),
}
}
}
impl<Fs: UniversalReadFs> Debug for CachedFs<Fs> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let Self {
fs,
prefix_path,
files_info,
files_prefetched,
} = self;
f.debug_struct("CachedReadFs")
.field("fs", fs)
.field("prefix_path", prefix_path)
.field("files_info", files_info)
.field("files_prefetched", &*files_prefetched.lock())
.finish()
}
}
impl<Fs: UniversalReadFs> UniversalReadFs for CachedFs<Fs> {
type File = Fs::File;
type OpenExtra = Fs::OpenExtra;
fn open(
&self,
path: impl AsRef<Path>,
options: OpenOptions,
extra: Self::OpenExtra,
) -> UioResult<Fs::File> {
let path = path.as_ref();
if options.writeable {
return Err(UniversalIoError::Uninitialized {
description:
"CachedReadFs only supports read-only files, writeable option is not allowed"
.to_string(),
});
}
if let Some(file) = self.files_prefetched.lock().remove(path) {
return Ok(file);
}
if let Some(files_info) = &self.files_info
&& !files_info.contains_key(path)
{
return Err(UniversalIoError::NotFound {
path: path.to_path_buf(),
});
}
let extra = match self.file_info(path) {
Some(info) => extra.with_known_len(info.size),
None => extra,
};
self.fs.open(path, options, extra)
}
}