mod abi;
mod async_io;
mod content_store;
pub mod manager;
mod size_store;
pub mod store;
mod tree_store;
use std::{ffi::OsStr, sync::Arc, time::Duration};
pub use manager::DicfuseManager;
use crate::util::config;
pub(crate) fn compute_store_dir_for_base_path_with_store_root(
store_root: &str,
base_path: &str,
) -> String {
let normalized = if base_path.is_empty() || base_path == "/" {
"/".to_string()
} else {
base_path.trim_end_matches('/').to_string()
};
if normalized == "/" {
store_root.to_string()
} else {
let digest = ring::digest::digest(&ring::digest::SHA256, normalized.as_bytes());
let hex = hex::encode(digest.as_ref());
format!("{}/dicfuse/{}", store_root, &hex[..16])
}
}
use async_trait::async_trait;
use libfuse_fs::{
context::OperationContext,
unionfs::{layer::Layer, Inode},
};
use rfuse3::{
raw::reply::{ReplyCreated, ReplyEntry},
Result,
};
use store::DictionaryStore;
use tree_store::StorageItem;
pub struct Dicfuse {
readable: bool,
pub store: Arc<DictionaryStore>,
}
unsafe impl Sync for Dicfuse {}
unsafe impl Send for Dicfuse {}
#[async_trait]
impl Layer for Dicfuse {
fn root_inode(&self) -> Inode {
1
}
async fn create_with_context(
&self,
_ctx: OperationContext,
_parent: Inode,
_name: &OsStr,
_mode: u32,
_flags: u32,
) -> Result<ReplyCreated> {
tracing::warn!(
"[{}:{}] create_with_context not supported on Dicfuse (read-only)",
file!(),
line!()
);
Err(std::io::Error::from_raw_os_error(libc::EROFS).into())
}
async fn mkdir_with_context(
&self,
_ctx: OperationContext,
_parent: Inode,
_name: &OsStr,
_mode: u32,
_umask: u32,
) -> Result<ReplyEntry> {
tracing::warn!(
"[{}:{}] mkdir_with_context not supported on Dicfuse (read-only)",
file!(),
line!()
);
Err(std::io::Error::from_raw_os_error(libc::EROFS).into())
}
async fn symlink_with_context(
&self,
_ctx: OperationContext,
_parent: Inode,
_name: &OsStr,
_link: &OsStr,
) -> Result<ReplyEntry> {
tracing::warn!(
"[{}:{}] symlink_with_context not supported on Dicfuse (read-only)",
file!(),
line!()
);
Err(std::io::Error::from_raw_os_error(libc::EROFS).into())
}
async fn getattr_with_mapping(
&self,
inode: Inode,
_handle: Option<u64>,
_mapping: bool,
) -> std::io::Result<(libc::stat64, std::time::Duration)> {
let item = self
.store
.get_inode(inode)
.await
.map_err(|_| std::io::Error::from_raw_os_error(libc::ENOENT))?;
let attr = item.get_stat().attr;
let size: i64 = if item.is_dir() {
0
} else {
self.store
.get_or_fetch_file_size(inode, &item.hash)
.await
.min(i64::MAX as u64) as i64
};
let type_bits: libc::mode_t = match attr.kind {
rfuse3::FileType::Directory => libc::S_IFDIR,
rfuse3::FileType::Symlink => libc::S_IFLNK,
_ => libc::S_IFREG,
};
let perm: libc::mode_t = if item.is_dir() {
attr.perm as libc::mode_t
} else if self.store.is_executable(inode) {
0o755
} else {
0o644
};
let mode: libc::mode_t = type_bits | perm;
let nlink = if attr.nlink > 0 {
attr.nlink
} else if item.is_dir() {
2
} else {
1
};
let mut stat: libc::stat64 = unsafe { std::mem::zeroed() };
stat.st_dev = 0;
stat.st_ino = inode;
stat.st_nlink = nlink as _;
stat.st_mode = mode;
stat.st_uid = attr.uid;
stat.st_gid = attr.gid;
stat.st_rdev = 0;
stat.st_size = size;
stat.st_blksize = 4096;
stat.st_blocks = (size + 511) / 512; stat.st_atime = attr.atime.sec;
stat.st_atime_nsec = attr.atime.nsec.into();
stat.st_mtime = attr.mtime.sec;
stat.st_mtime_nsec = attr.mtime.nsec.into();
stat.st_ctime = attr.ctime.sec;
stat.st_ctime_nsec = attr.ctime.nsec.into();
Ok((stat, self.reply_ttl()))
}
}
#[allow(unused)]
impl Dicfuse {
pub async fn new() -> Self {
Self {
readable: config::dicfuse_readable(),
store: DictionaryStore::new().await.into(), }
}
pub fn start_import(&self) {
if self.store.try_start_import() {
let s = self.store.clone();
tokio::spawn(async move {
store::import_arc(s).await;
});
}
}
pub async fn new_with_store_path(store_path: &str) -> Self {
Self {
readable: config::dicfuse_readable(),
store: DictionaryStore::new_with_store_path(store_path)
.await
.into(),
}
}
pub async fn new_with_base_path_and_store_path(base_path: &str, store_path: &str) -> Self {
Self {
readable: config::dicfuse_readable(),
store: DictionaryStore::new_with_base_path_and_store_path(base_path, store_path)
.await
.into(),
}
}
pub async fn new_with_base_path(base_path: &str) -> Self {
Self {
readable: config::dicfuse_readable(),
store: DictionaryStore::new_with_base_path(base_path).await.into(),
}
}
pub fn base_path(&self) -> &str {
self.store.base_path()
}
pub(crate) fn reply_ttl(&self) -> Duration {
let is_subdir_mount = !(self.base_path().is_empty() || self.base_path() == "/");
let ttl_secs = if is_subdir_mount {
config::antares_dicfuse_reply_ttl_secs()
} else {
config::dicfuse_reply_ttl_secs()
};
Duration::from_secs(ttl_secs)
}
pub async fn get_stat(&self, item: StorageItem) -> ReplyEntry {
let mut e = item.get_stat();
e.ttl = self.reply_ttl();
if item.is_dir() {
e.attr.size = 0;
return e;
}
let size = self
.store
.file_size_for_stat(item.get_inode(), &item.hash)
.await;
e.attr.size = size;
e
}
pub async fn get_stat_fast(&self, item: StorageItem) -> ReplyEntry {
let mut e = item.get_stat();
e.ttl = self.reply_ttl();
if item.is_dir() {
e.attr.size = 0;
return e;
}
e.attr.size = self.store.get_persisted_size(item.get_inode()).unwrap_or(0);
e
}
}
#[cfg(test)]
mod tests {
use std::{ffi::OsStr, path::PathBuf};
use libfuse_fs::unionfs::layer::Layer;
use tokio::signal;
use crate::dicfuse::Dicfuse;
#[tokio::test]
#[ignore = "manual test requiring root privileges for FUSE mount"]
async fn test_mount_dic() {
let mount_path =
std::env::var("DIC_MOUNT_PATH").unwrap_or_else(|_| "/tmp/test_dic_mount".to_string());
std::fs::create_dir_all(&mount_path).expect("Failed to create mount directory");
let fs = Dicfuse::new().await;
let mountpoint = OsStr::new(&mount_path);
let mut mount_handle = crate::server::mount_filesystem(fs, mountpoint).await;
let handle = &mut mount_handle;
tokio::select! {
res = handle => res.unwrap(),
_ = signal::ctrl_c() => {
mount_handle.unmount().await.unwrap()
}
}
}
#[tokio::test]
async fn test_getattr_with_mapping_preserves_mode_and_size() {
let base = PathBuf::from("/tmp/dicfuse_attr_test");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let dic = Dicfuse::new_with_store_path(base.to_str().unwrap()).await;
dic.store.insert_mock_item(1, 0, "", true).await;
dic.store.insert_mock_item(2, 1, "file", false).await;
dic.store.save_file(2, b"abc".to_vec());
dic.store.set_executable(2, true);
let (file_stat, _) = dic.getattr_with_mapping(2, None, false).await.unwrap();
assert_eq!(file_stat.st_mode & libc::S_IFMT, libc::S_IFREG);
assert_eq!(file_stat.st_mode & 0o777, 0o755);
assert_eq!(file_stat.st_size, 3);
let (dir_stat, _) = dic.getattr_with_mapping(1, None, false).await.unwrap();
assert_eq!(dir_stat.st_mode & libc::S_IFMT, libc::S_IFDIR);
assert_eq!(dir_stat.st_mode & 0o777, 0o755);
assert_eq!(dir_stat.st_nlink, 2);
let _ = std::fs::remove_dir_all(&base);
}
}