use std::{
cell::RefCell,
collections::HashMap,
ffi::{OsStr, OsString},
path::Path,
sync::{
Arc, Mutex, RwLock,
atomic::{AtomicU64, Ordering},
},
time::{Duration, Instant, SystemTime},
};
thread_local! {
static READ_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(128 * 1024));
}
use chrono::{DateTime, Utc};
use fuse_mt::{
CallbackResult, DirectoryEntry as FuseDirectoryEntry, FileAttr, FileType, FilesystemMT,
RequestInfo, ResultData, ResultEmpty, ResultEntry, ResultOpen, ResultReaddir, ResultSlice,
ResultStatfs, ResultXattr, Statfs,
};
static NEXT_DIR_FD: AtomicU64 = AtomicU64::new(0x1000);
use crate::{
common::{CvmfsError, CvmfsResult, FileLike},
directory_entry::DirectoryEntry,
repository::Repository,
};
const FOPEN_KEEP_CACHE: u32 = 0x02;
const TTL: Duration = Duration::from_secs(3600);
#[allow(clippy::unnecessary_cast)]
fn map_dirent_type_to_fs_kind(dirent: &DirectoryEntry) -> FileType {
if dirent.is_directory() {
FileType::Directory
} else if dirent.is_symlink() {
FileType::Symlink
} else {
let mode = dirent.mode as u32;
let ifmt = libc::S_IFMT as u32;
match mode & ifmt {
m if m == libc::S_IFSOCK as u32 => FileType::Socket,
m if m == libc::S_IFIFO as u32 => FileType::NamedPipe,
m if m == libc::S_IFBLK as u32 => FileType::BlockDevice,
m if m == libc::S_IFCHR as u32 => FileType::CharDevice,
_ => FileType::RegularFile,
}
}
}
#[derive(Debug)]
pub struct CernvmFileSystem {
repository: RwLock<Repository>,
opened_files: RwLock<HashMap<String, Box<dyn FileLike>>>,
cached_statfs: Mutex<Option<(Instant, Statfs)>>,
lookup_cache: RwLock<HashMap<String, Arc<DirectoryEntry>>>,
readdir_cache: RwLock<HashMap<String, Vec<FuseDirectoryEntry>>>,
}
impl FilesystemMT for CernvmFileSystem {
fn destroy(&self) {
if let Ok(mut f) = self.opened_files.write() {
f.drain();
};
}
fn getattr(&self, _req: RequestInfo, path: &Path, _fh: Option<u64>) -> ResultEntry {
let path = path.to_str().ok_or(CvmfsError::FileNotFound)?;
log::info!("Getting attribute of path: {path}");
let result = self.cached_lookup(path)?;
let date_time: DateTime<Utc> =
DateTime::from_timestamp(result.mtime, 0).ok_or(CvmfsError::InvalidTimestamp)?;
let time = SystemTime::from(date_time);
let size = result.size as u64;
let nlink = result.nlink();
let file_attr = FileAttr {
size,
blocks: 1 + size / 512,
atime: time,
mtime: time,
ctime: time,
crtime: time,
kind: map_dirent_type_to_fs_kind(&result),
perm: result.mode & 0o7777,
nlink,
uid: result.uid,
gid: result.gid,
rdev: 0,
flags: 0,
};
Ok((TTL, file_attr))
}
fn readlink(&self, _req: RequestInfo, path: &Path) -> ResultData {
let path = path.to_str().ok_or(libc::ENOENT)?;
log::info!("Reading link: {path}");
if let Some(target) = self
.lookup_cache
.read()
.ok()
.and_then(|c| c.get(path).cloned())
.filter(|e| e.is_symlink())
.and_then(|e| e.symlink.clone())
{
return Ok(target.into_bytes());
}
let result = self.cached_lookup(path)?;
if !result.is_symlink() {
return Err(libc::ENOLINK);
}
Ok(result.symlink.as_ref().ok_or(libc::ENOLINK)?.clone().into_bytes())
}
fn open(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen {
let path = path.to_str().ok_or(CvmfsError::FileNotFound)?;
log::info!("Opening file: {path}");
let entry = self.cached_lookup(path)?;
if !entry.is_file() {
return Err(libc::ENOENT);
}
let repo = self.repository.read().map_err(|_| CvmfsError::Sync)?;
let file = repo.retrieve_object(&entry, path)?;
let fd = file.as_raw_fd() as u64;
drop(repo);
self.opened_files
.write()
.map_err(|_| CvmfsError::Sync)?
.insert(path.into(), file);
Ok((fd, FOPEN_KEEP_CACHE))
}
fn read(
&self,
_req: RequestInfo,
path: &Path,
_fh: u64,
offset: u64,
size: u32,
callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult,
) -> CallbackResult {
let path = match path.to_str() {
Some(p) => p,
None => return callback(Err(libc::ENOENT)),
};
log::info!("Reading file: {path}");
let opened_files = match self.opened_files.read() {
Ok(guard) => guard,
Err(e) => {
log::error!("{:?}", e);
return callback(Err(libc::EIO));
}
};
let file = match opened_files.get(path) {
Some(f) => f,
None => return callback(Err(libc::ENOENT)),
};
READ_BUF.with(|buf| {
let mut data = buf.borrow_mut();
data.resize(size as usize, 0);
let bytes_read = match file.read_at(offset, &mut data) {
Ok(n) => n,
Err(e) => {
log::error!("{:?}", e);
return callback(Err(match e.raw_os_error() {
Some(code) => code,
None => libc::EIO,
}));
}
};
callback(Ok(&data[0..bytes_read]))
})
}
fn flush(&self, _req: RequestInfo, path: &Path, _fh: u64, _lock_owner: u64) -> ResultEmpty {
let path = path.to_str().ok_or(libc::ENOENT)?;
log::info!("Flushing file: {path}");
Ok(())
}
fn release(
&self,
_req: RequestInfo,
path: &Path,
_fh: u64,
_flags: u32,
_lock_owner: u64,
_flush: bool,
) -> ResultEmpty {
let path = path.to_str().ok_or(libc::ENOENT)?;
log::info!("Releasing: {path}");
match self
.opened_files
.write()
.map_err(|e| {
log::error!("{:?}", e);
libc::EIO
})?
.remove(path)
{
None => Err(libc::ENOENT),
Some(_) => Ok(()),
}
}
fn opendir(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen {
let path = path.to_str().ok_or(libc::ENOENT)?;
log::info!("Opening directory: {path}");
let result = self.cached_lookup(path)?;
if !result.is_directory() {
return Err(libc::ENOENT);
}
let fd = NEXT_DIR_FD.fetch_add(1, Ordering::Relaxed);
Ok((fd, 0))
}
fn readdir(&self, _req: RequestInfo, path: &Path, _fh: u64) -> ResultReaddir {
let path = path.to_str().ok_or(libc::ENOENT)?;
log::info!("Reading directory: {path}");
if let Some(entries) = self.readdir_cache.read().ok().and_then(|c| c.get(path).cloned()) {
return Ok(entries);
}
let repo = self.repository.read().map_err(|_| libc::EIO)?;
match repo.list_directory(path) {
Ok(entries) => {
drop(repo);
if let Ok(mut cache) = self.lookup_cache.write() {
for entry in &entries {
let child_path = if path == "/" {
format!("/{}", entry.name)
} else {
format!("{}/{}", path, entry.name)
};
cache.insert(child_path, Arc::new(entry.clone()));
}
}
let fuse_entries: Vec<FuseDirectoryEntry> = entries
.into_iter()
.map(|dirent| FuseDirectoryEntry {
kind: map_dirent_type_to_fs_kind(&dirent),
name: OsString::from(dirent.name),
})
.collect();
if let Ok(mut cache) = self.readdir_cache.write() {
cache.insert(path.into(), fuse_entries.clone());
}
Ok(fuse_entries)
}
Err(e) => {
log::error!("Could not list directory {path}: {:?}", e);
Err(e.into())
}
}
}
fn releasedir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32) -> ResultEmpty {
Ok(())
}
fn statfs(&self, _req: RequestInfo, _path: &Path) -> ResultStatfs {
if let Some((ts, cached)) = *self.cached_statfs.lock().map_err(|_| libc::EIO)? {
#[allow(clippy::collapsible_if)]
if ts.elapsed() < Duration::from_secs(5) {
return Ok(cached);
}
}
log::info!("Refreshing FS statistics");
let repo = self.repository.read().map_err(|_| libc::EIO)?;
let statistics = repo.get_statistics()?;
let result = Statfs {
blocks: 1 + statistics.file_size as u64 / 512,
bfree: 0,
bavail: 0,
files: statistics.regular as u64,
ffree: 0,
bsize: 512,
namelen: 255,
frsize: 512,
};
drop(repo);
if let Ok(mut cache) = self.cached_statfs.lock() {
*cache = Some((Instant::now(), result));
}
Ok(result)
}
fn getxattr(&self, _req: RequestInfo, _path: &Path, name: &OsStr, size: u32) -> ResultXattr {
let name = name.to_str().ok_or(libc::ENODATA)?;
let repo = self.repository.read().map_err(|_| libc::EIO)?;
let value = match name {
"user.fqrn" => repo.fqrn.clone(),
"user.revision" => repo.manifest.revision.to_string(),
"user.hash" => repo.manifest.root_catalog.clone(),
"user.host" => repo.fetcher_source(),
"user.expires" => repo.manifest.last_modified.to_rfc3339(),
"user.nclg" => repo.opened_catalogs.read().map(|c| c.len()).unwrap_or(0).to_string(),
_ => return Err(libc::ENODATA),
};
let bytes = value.into_bytes();
if size == 0 {
return Ok(fuse_mt::Xattr::Size(bytes.len() as u32));
}
Ok(fuse_mt::Xattr::Data(bytes))
}
fn access(&self, _req: RequestInfo, path: &Path, _mask: u32) -> ResultEmpty {
let path = path.to_str().ok_or(libc::ENOENT)?;
log::info!("Accessing: {path}");
self.cached_lookup(path).map(|_| ())?;
Ok(())
}
}
impl CernvmFileSystem {
fn cached_lookup(&self, path: &str) -> CvmfsResult<Arc<DirectoryEntry>> {
if let Some(entry) = self.lookup_cache.read().ok().and_then(|c| c.get(path).cloned()) {
return Ok(entry);
}
let repo = self.repository.read().map_err(|e| CvmfsError::Generic(format!("{:?}", e)))?;
let entry = Arc::new(repo.lookup(path)?);
drop(repo);
if let Ok(mut cache) = self.lookup_cache.write() {
cache.insert(path.into(), Arc::clone(&entry));
}
Ok(entry)
}
pub fn new(repository: Repository) -> CvmfsResult<Self> {
Ok(Self {
repository: RwLock::new(repository),
opened_files: Default::default(),
cached_statfs: Mutex::new(None),
lookup_cache: Default::default(),
readdir_cache: Default::default(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::directory_entry::Flags;
fn make_entry(flags: u32, mode: u16) -> DirectoryEntry {
DirectoryEntry {
md5_path_1: 0,
md5_path_2: 0,
parent_1: 0,
parent_2: 0,
content_hash: None,
flags,
size: 0,
mode,
mtime: 0,
name: String::new(),
symlink: None,
uid: 0,
gid: 0,
xattr: None,
content_hash_type: crate::directory_entry::ContentHashTypes::Sha1,
chunks: Vec::new(),
hardlinks: 0,
}
}
#[test]
fn map_type_directory() {
let entry = make_entry(Flags::Directory as u32, 0o40755);
assert_eq!(map_dirent_type_to_fs_kind(&entry), FileType::Directory);
}
#[test]
fn map_type_symlink() {
let entry = make_entry(Flags::Link as u32, 0o120777);
assert_eq!(map_dirent_type_to_fs_kind(&entry), FileType::Symlink);
}
#[test]
fn map_type_regular_file() {
let entry = make_entry(Flags::File as u32, 0o100644);
assert_eq!(map_dirent_type_to_fs_kind(&entry), FileType::RegularFile);
}
#[test]
fn map_type_socket() {
let entry = make_entry(Flags::File as u32, 0o140755);
assert_eq!(map_dirent_type_to_fs_kind(&entry), FileType::Socket);
}
#[test]
fn map_type_named_pipe() {
let entry = make_entry(Flags::File as u32, 0o010644);
assert_eq!(map_dirent_type_to_fs_kind(&entry), FileType::NamedPipe);
}
#[test]
fn map_type_block_device() {
let entry = make_entry(Flags::File as u32, 0o060660);
assert_eq!(map_dirent_type_to_fs_kind(&entry), FileType::BlockDevice);
}
#[test]
fn map_type_char_device() {
let entry = make_entry(Flags::File as u32, 0o020666);
assert_eq!(map_dirent_type_to_fs_kind(&entry), FileType::CharDevice);
}
#[test]
fn map_type_zero_mode_defaults_to_regular() {
let entry = make_entry(Flags::File as u32, 0);
assert_eq!(map_dirent_type_to_fs_kind(&entry), FileType::RegularFile);
}
}