use super::{BoxedFile, DirEntry, File, FileSystem, FilesystemStats, FsError, Stats, TimeChange};
use crate::error::{Error, Result};
use async_trait::async_trait;
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::sync::RwLock;
pub const ROOT_INO: i64 = 1;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct SrcId {
ino: u64,
dev: u64,
}
struct Inode {
fd: OwnedFd,
src_ino: u64,
#[allow(dead_code)]
src_dev: u64,
nlookup: AtomicU64,
}
pub struct HostFS {
root: PathBuf,
root_fd: OwnedFd,
inodes: RwLock<HashMap<i64, Inode>>,
src_to_ino: RwLock<HashMap<SrcId, i64>>,
next_ino: AtomicU64,
#[cfg(target_family = "unix")]
fuse_mountpoint_inode: Option<u64>,
}
pub struct HostFSFile {
fd: OwnedFd,
}
#[async_trait]
impl File for HostFSFile {
async fn pread(&self, offset: u64, size: u64) -> Result<Vec<u8>> {
let fd = self.fd.as_raw_fd();
tokio::task::spawn_blocking(move || {
let mut buf = vec![0u8; size as usize];
let n = unsafe {
libc::pread(
fd,
buf.as_mut_ptr() as *mut libc::c_void,
size as usize,
offset as libc::off_t,
)
};
if n < 0 {
return Err(std::io::Error::last_os_error().into());
}
buf.truncate(n as usize);
Ok(buf)
})
.await
.map_err(|e| Error::Internal(e.to_string()))?
}
async fn pwrite(&self, offset: u64, data: &[u8]) -> Result<()> {
let fd = self.fd.as_raw_fd();
let data = data.to_vec();
tokio::task::spawn_blocking(move || {
let n = unsafe {
libc::pwrite(
fd,
data.as_ptr() as *const libc::c_void,
data.len(),
offset as libc::off_t,
)
};
if n < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
})
.await
.map_err(|e| Error::Internal(e.to_string()))?
}
async fn truncate(&self, size: u64) -> Result<()> {
let fd = self.fd.as_raw_fd();
tokio::task::spawn_blocking(move || {
let result = unsafe { libc::ftruncate(fd, size as libc::off_t) };
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
})
.await
.map_err(|e| Error::Internal(e.to_string()))?
}
async fn fsync(&self) -> Result<()> {
let fd = self.fd.as_raw_fd();
tokio::task::spawn_blocking(move || {
let result = unsafe { libc::fsync(fd) };
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
})
.await
.map_err(|e| Error::Internal(e.to_string()))?
}
async fn fstat(&self) -> Result<Stats> {
let fd = self.fd.as_raw_fd();
tokio::task::spawn_blocking(move || {
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let result = unsafe { libc::fstat(fd, &mut stat) };
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(stat_to_stats(&stat))
})
.await
.map_err(|e| Error::Internal(e.to_string()))?
}
}
fn stat_to_stats(stat: &libc::stat) -> Stats {
Stats {
ino: stat.st_ino as i64,
mode: stat.st_mode,
nlink: stat.st_nlink as u32,
uid: stat.st_uid,
gid: stat.st_gid,
size: stat.st_size,
atime: stat.st_atime,
mtime: stat.st_mtime,
ctime: stat.st_ctime,
atime_nsec: stat.st_atime_nsec as u32,
mtime_nsec: stat.st_mtime_nsec as u32,
ctime_nsec: stat.st_ctime_nsec as u32,
rdev: stat.st_rdev,
}
}
impl HostFS {
pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
let root = root.into();
if !root.exists() {
return Err(Error::BaseDirectoryNotFound(root.display().to_string()));
}
if !root.is_dir() {
return Err(Error::NotADirectory(root.display().to_string()));
}
let c_path = CString::new(root.as_os_str().as_bytes())
.map_err(|_| Error::Internal("invalid path".to_string()))?;
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_PATH | libc::O_DIRECTORY) };
if fd < 0 {
return Err(std::io::Error::last_os_error().into());
}
let root_fd = unsafe { OwnedFd::from_raw_fd(fd) };
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let result = unsafe {
libc::fstatat(
root_fd.as_raw_fd(),
c"".as_ptr(),
&mut stat,
libc::AT_EMPTY_PATH,
)
};
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
let root_inode = Inode {
fd: root_fd
.try_clone()
.map_err(|e| Error::Internal(e.to_string()))?,
src_ino: stat.st_ino,
src_dev: stat.st_dev,
nlookup: AtomicU64::new(1),
};
let mut inodes = HashMap::new();
inodes.insert(ROOT_INO, root_inode);
let mut src_to_ino = HashMap::new();
src_to_ino.insert(
SrcId {
ino: stat.st_ino,
dev: stat.st_dev,
},
ROOT_INO,
);
Ok(Self {
root,
root_fd,
inodes: RwLock::new(inodes),
src_to_ino: RwLock::new(src_to_ino),
next_ino: AtomicU64::new(2),
fuse_mountpoint_inode: None,
})
}
#[cfg(target_family = "unix")]
pub fn with_fuse_mountpoint(mut self, inode: u64) -> Self {
self.fuse_mountpoint_inode = Some(inode);
self
}
pub fn root(&self) -> &PathBuf {
&self.root
}
fn get_inode_fd(&self, ino: i64) -> Result<RawFd> {
let inodes = self.inodes.read().unwrap();
let inode = inodes.get(&ino).ok_or(FsError::NotFound)?;
Ok(inode.fd.as_raw_fd())
}
fn alloc_ino(&self) -> i64 {
self.next_ino.fetch_add(1, Ordering::Relaxed) as i64
}
fn fstatat_empty_path(fd: RawFd) -> Result<libc::stat> {
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let result = unsafe {
libc::fstatat(
fd,
c"".as_ptr(),
&mut stat,
libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW,
)
};
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(stat)
}
fn open_real_fd(o_path_fd: RawFd, flags: libc::c_int) -> Result<OwnedFd> {
let proc_path = format!("/proc/self/fd/{}\0", o_path_fd);
let fd = unsafe { libc::open(proc_path.as_ptr() as *const libc::c_char, flags) };
if fd < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
fn get_or_create_inode(&self, fd: OwnedFd, stat: &libc::stat) -> (i64, bool) {
let src_id = SrcId {
ino: stat.st_ino,
dev: stat.st_dev,
};
{
let src_map = self.src_to_ino.read().unwrap();
if let Some(&ino) = src_map.get(&src_id) {
let inodes = self.inodes.read().unwrap();
if let Some(inode) = inodes.get(&ino) {
inode.nlookup.fetch_add(1, Ordering::Relaxed);
return (ino, false);
}
}
}
let ino = self.alloc_ino();
let inode = Inode {
fd,
src_ino: stat.st_ino,
src_dev: stat.st_dev,
nlookup: AtomicU64::new(1),
};
{
let mut inodes = self.inodes.write().unwrap();
inodes.insert(ino, inode);
}
{
let mut src_map = self.src_to_ino.write().unwrap();
src_map.insert(src_id, ino);
}
(ino, true)
}
#[allow(dead_code)]
fn remove_inode(&self, ino: i64) {
let mut inodes = self.inodes.write().unwrap();
if let Some(inode) = inodes.remove(&ino) {
let mut src_map = self.src_to_ino.write().unwrap();
src_map.remove(&SrcId {
ino: inode.src_ino,
dev: inode.src_dev,
});
}
}
}
#[async_trait]
impl FileSystem for HostFS {
async fn lookup(&self, parent_ino: i64, name: &str) -> Result<Option<Stats>> {
let parent_fd = self.get_inode_fd(parent_ino)?;
#[cfg(target_family = "unix")]
if let Some(fuse_ino) = self.fuse_mountpoint_inode {
let inodes = self.inodes.read().unwrap();
if let Some(parent_inode) = inodes.get(&parent_ino) {
if parent_inode.src_ino == fuse_ino {
return Ok(None);
}
}
}
let c_name = CString::new(name).map_err(|_| FsError::InvalidPath)?;
let child_fd =
unsafe { libc::openat(parent_fd, c_name.as_ptr(), libc::O_PATH | libc::O_NOFOLLOW) };
if child_fd < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::NotFound {
return Ok(None);
}
return Err(err.into());
}
let child_fd = unsafe { OwnedFd::from_raw_fd(child_fd) };
let stat = Self::fstatat_empty_path(child_fd.as_raw_fd())?;
#[cfg(target_family = "unix")]
if let Some(fuse_ino) = self.fuse_mountpoint_inode {
if stat.st_ino == fuse_ino {
return Ok(None);
}
}
let (ino, _is_new) = self.get_or_create_inode(child_fd, &stat);
let mut stats = stat_to_stats(&stat);
stats.ino = ino;
Ok(Some(stats))
}
async fn getattr(&self, ino: i64) -> Result<Option<Stats>> {
let fd = match self.get_inode_fd(ino) {
Ok(fd) => fd,
Err(_) => return Ok(None),
};
let stat = Self::fstatat_empty_path(fd)?;
let mut stats = stat_to_stats(&stat);
stats.ino = ino;
Ok(Some(stats))
}
async fn readlink(&self, ino: i64) -> Result<Option<String>> {
let fd = match self.get_inode_fd(ino) {
Ok(fd) => fd,
Err(_) => return Ok(None),
};
let mut buf = vec![0u8; libc::PATH_MAX as usize];
let c_empty = CString::new("").unwrap();
let len = unsafe {
libc::readlinkat(
fd,
c_empty.as_ptr(),
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
)
};
if len < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::NotFound {
return Ok(None);
}
if err.raw_os_error() == Some(libc::EINVAL) {
return Err(FsError::NotASymlink.into());
}
return Err(err.into());
}
buf.truncate(len as usize);
Ok(Some(String::from_utf8_lossy(&buf).to_string()))
}
async fn readdir(&self, ino: i64) -> Result<Option<Vec<String>>> {
let fd = match self.get_inode_fd(ino) {
Ok(fd) => fd,
Err(_) => return Ok(None),
};
let dir_fd = Self::open_real_fd(fd, libc::O_RDONLY | libc::O_DIRECTORY)?;
tokio::task::spawn_blocking(move || {
let dir = unsafe { libc::fdopendir(dir_fd.as_raw_fd()) };
if dir.is_null() {
return Err::<_, Error>(std::io::Error::last_os_error().into());
}
std::mem::forget(dir_fd);
let mut entries = Vec::new();
loop {
unsafe { *libc::__errno_location() = 0 };
let entry = unsafe { libc::readdir(dir) };
if entry.is_null() {
let errno = unsafe { *libc::__errno_location() };
if errno != 0 {
unsafe { libc::closedir(dir) };
return Err(std::io::Error::from_raw_os_error(errno).into());
}
break;
}
let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) };
let name_str = name.to_string_lossy();
if name_str == "." || name_str == ".." {
continue;
}
entries.push(name_str.to_string());
}
unsafe { libc::closedir(dir) };
entries.sort();
Ok(Some(entries))
})
.await
.map_err(|e| Error::Internal(e.to_string()))?
}
async fn readdir_plus(&self, ino: i64) -> Result<Option<Vec<DirEntry>>> {
let fd = match self.get_inode_fd(ino) {
Ok(fd) => fd,
Err(_) => return Ok(None),
};
let dir_fd = Self::open_real_fd(fd, libc::O_RDONLY | libc::O_DIRECTORY)?;
let dir_fd_raw = dir_fd.as_raw_fd();
#[cfg(target_family = "unix")]
let fuse_mountpoint_inode = self.fuse_mountpoint_inode;
let entries_raw: Vec<(String, libc::stat)> = tokio::task::spawn_blocking(move || {
let dir = unsafe { libc::fdopendir(dir_fd.as_raw_fd()) };
if dir.is_null() {
return Err::<_, Error>(std::io::Error::last_os_error().into());
}
std::mem::forget(dir_fd);
let mut entries = Vec::new();
loop {
unsafe { *libc::__errno_location() = 0 };
let entry = unsafe { libc::readdir(dir) };
if entry.is_null() {
let errno = unsafe { *libc::__errno_location() };
if errno != 0 {
unsafe { libc::closedir(dir) };
return Err(std::io::Error::from_raw_os_error(errno).into());
}
break;
}
let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) };
let name_str = name.to_string_lossy();
if name_str == "." || name_str == ".." {
continue;
}
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let result = unsafe {
libc::fstatat(
dir_fd_raw,
(*entry).d_name.as_ptr(),
&mut stat,
libc::AT_SYMLINK_NOFOLLOW,
)
};
if result == 0 {
#[cfg(target_family = "unix")]
if let Some(fuse_ino) = fuse_mountpoint_inode {
if stat.st_ino == fuse_ino {
continue;
}
}
entries.push((name_str.to_string(), stat));
}
}
unsafe { libc::closedir(dir) };
Ok(entries)
})
.await
.map_err(|e| Error::Internal(e.to_string()))??;
let mut result = Vec::new();
for (name, stat) in entries_raw {
let c_name = CString::new(name.as_str()).map_err(|_| FsError::InvalidPath)?;
let child_fd =
unsafe { libc::openat(fd, c_name.as_ptr(), libc::O_PATH | libc::O_NOFOLLOW) };
if child_fd < 0 {
continue;
}
let child_fd = unsafe { OwnedFd::from_raw_fd(child_fd) };
let (child_ino, _) = self.get_or_create_inode(child_fd, &stat);
let mut stats = stat_to_stats(&stat);
stats.ino = child_ino;
result.push(DirEntry { name, stats });
}
result.sort_by(|a, b| a.name.cmp(&b.name));
Ok(Some(result))
}
async fn chmod(&self, ino: i64, mode: u32) -> Result<()> {
let fd = self.get_inode_fd(ino)?;
let proc_path = CString::new(format!("/proc/self/fd/{}", fd))
.map_err(|_| Error::Internal("invalid path".to_string()))?;
let result = unsafe { libc::chmod(proc_path.as_ptr(), mode as libc::mode_t) };
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
async fn chown(&self, ino: i64, uid: Option<u32>, gid: Option<u32>) -> Result<()> {
let fd = self.get_inode_fd(ino)?;
let stat = Self::fstatat_empty_path(fd)?;
let uid = uid.unwrap_or(stat.st_uid);
let gid = gid.unwrap_or(stat.st_gid);
let result = unsafe {
libc::fchownat(
fd,
c"".as_ptr(),
uid,
gid,
libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW,
)
};
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
async fn utimens(&self, ino: i64, atime: TimeChange, mtime: TimeChange) -> Result<()> {
let fd = self.get_inode_fd(ino)?;
let to_timespec = |tc: TimeChange, current: libc::timespec| -> libc::timespec {
match tc {
TimeChange::Set(secs, nsec) => libc::timespec {
tv_sec: secs as libc::time_t,
tv_nsec: nsec as libc::c_long,
},
TimeChange::Now => libc::timespec {
tv_sec: 0,
tv_nsec: libc::UTIME_NOW,
},
TimeChange::Omit => current,
}
};
let omit_spec = libc::timespec {
tv_sec: 0,
tv_nsec: libc::UTIME_OMIT,
};
let times = [to_timespec(atime, omit_spec), to_timespec(mtime, omit_spec)];
let proc_path = CString::new(format!("/proc/self/fd/{}", fd))
.map_err(|_| Error::Internal("invalid path".to_string()))?;
let result =
unsafe { libc::utimensat(libc::AT_FDCWD, proc_path.as_ptr(), times.as_ptr(), 0) };
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
async fn open(&self, ino: i64, flags: i32) -> Result<BoxedFile> {
let fd = self.get_inode_fd(ino)?;
let real_fd = Self::open_real_fd(fd, flags)?;
Ok(Arc::new(HostFSFile { fd: real_fd }))
}
async fn mkdir(
&self,
parent_ino: i64,
name: &str,
mode: u32,
_uid: u32,
_gid: u32,
) -> Result<Stats> {
let parent_fd = self.get_inode_fd(parent_ino)?;
let c_name = CString::new(name).map_err(|_| FsError::InvalidPath)?;
let result = unsafe { libc::mkdirat(parent_fd, c_name.as_ptr(), mode as libc::mode_t) };
if result < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EEXIST) {
return Err(FsError::AlreadyExists.into());
}
return Err(err.into());
}
self.lookup(parent_ino, name)
.await?
.ok_or(FsError::NotFound.into())
}
async fn create_file(
&self,
parent_ino: i64,
name: &str,
mode: u32,
_uid: u32,
_gid: u32,
) -> Result<(Stats, BoxedFile)> {
let parent_fd = self.get_inode_fd(parent_ino)?;
let c_name = CString::new(name).map_err(|_| FsError::InvalidPath)?;
let file_fd = unsafe {
libc::openat(
parent_fd,
c_name.as_ptr(),
libc::O_CREAT | libc::O_EXCL | libc::O_RDWR,
mode as libc::mode_t,
)
};
if file_fd < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EEXIST) {
return Err(FsError::AlreadyExists.into());
}
return Err(err.into());
}
let real_fd = unsafe { OwnedFd::from_raw_fd(file_fd) };
let o_path_fd =
unsafe { libc::openat(parent_fd, c_name.as_ptr(), libc::O_PATH | libc::O_NOFOLLOW) };
if o_path_fd < 0 {
return Err(std::io::Error::last_os_error().into());
}
let o_path_fd = unsafe { OwnedFd::from_raw_fd(o_path_fd) };
let stat = Self::fstatat_empty_path(o_path_fd.as_raw_fd())?;
let (ino, _) = self.get_or_create_inode(o_path_fd, &stat);
let mut stats = stat_to_stats(&stat);
stats.ino = ino;
let file: BoxedFile = Arc::new(HostFSFile { fd: real_fd });
Ok((stats, file))
}
async fn mknod(
&self,
parent_ino: i64,
name: &str,
mode: u32,
rdev: u64,
_uid: u32,
_gid: u32,
) -> Result<Stats> {
let parent_fd = self.get_inode_fd(parent_ino)?;
let c_name = CString::new(name).map_err(|_| FsError::InvalidPath)?;
let result = unsafe {
libc::mknodat(
parent_fd,
c_name.as_ptr(),
mode as libc::mode_t,
rdev as libc::dev_t,
)
};
if result < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EEXIST) {
return Err(FsError::AlreadyExists.into());
}
return Err(err.into());
}
self.lookup(parent_ino, name)
.await?
.ok_or(FsError::NotFound.into())
}
async fn symlink(
&self,
parent_ino: i64,
name: &str,
target: &str,
_uid: u32,
_gid: u32,
) -> Result<Stats> {
let parent_fd = self.get_inode_fd(parent_ino)?;
let c_name = CString::new(name).map_err(|_| FsError::InvalidPath)?;
let c_target = CString::new(target).map_err(|_| FsError::InvalidPath)?;
let result = unsafe { libc::symlinkat(c_target.as_ptr(), parent_fd, c_name.as_ptr()) };
if result < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EEXIST) {
return Err(FsError::AlreadyExists.into());
}
return Err(err.into());
}
self.lookup(parent_ino, name)
.await?
.ok_or(FsError::NotFound.into())
}
async fn unlink(&self, parent_ino: i64, name: &str) -> Result<()> {
let parent_fd = self.get_inode_fd(parent_ino)?;
let c_name = CString::new(name).map_err(|_| FsError::InvalidPath)?;
let result = unsafe { libc::unlinkat(parent_fd, c_name.as_ptr(), 0) };
if result < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::NotFound {
return Err(FsError::NotFound.into());
}
return Err(err.into());
}
Ok(())
}
async fn rmdir(&self, parent_ino: i64, name: &str) -> Result<()> {
let parent_fd = self.get_inode_fd(parent_ino)?;
let c_name = CString::new(name).map_err(|_| FsError::InvalidPath)?;
let result = unsafe { libc::unlinkat(parent_fd, c_name.as_ptr(), libc::AT_REMOVEDIR) };
if result < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::NotFound {
return Err(FsError::NotFound.into());
}
if err.raw_os_error() == Some(libc::ENOTEMPTY) {
return Err(FsError::NotEmpty.into());
}
if err.raw_os_error() == Some(libc::ENOTDIR) {
return Err(FsError::NotADirectory.into());
}
return Err(err.into());
}
Ok(())
}
async fn link(&self, ino: i64, newparent_ino: i64, newname: &str) -> Result<Stats> {
let fd = self.get_inode_fd(ino)?;
let newparent_fd = self.get_inode_fd(newparent_ino)?;
let c_newname = CString::new(newname).map_err(|_| FsError::InvalidPath)?;
let result = unsafe {
libc::linkat(
fd,
c"".as_ptr(),
newparent_fd,
c_newname.as_ptr(),
libc::AT_EMPTY_PATH,
)
};
if result < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EEXIST) {
return Err(FsError::AlreadyExists.into());
}
return Err(err.into());
}
self.getattr(ino).await?.ok_or(FsError::NotFound.into())
}
async fn rename(
&self,
oldparent_ino: i64,
oldname: &str,
newparent_ino: i64,
newname: &str,
) -> Result<()> {
let oldparent_fd = self.get_inode_fd(oldparent_ino)?;
let newparent_fd = self.get_inode_fd(newparent_ino)?;
let c_oldname = CString::new(oldname).map_err(|_| FsError::InvalidPath)?;
let c_newname = CString::new(newname).map_err(|_| FsError::InvalidPath)?;
let result = unsafe {
libc::renameat(
oldparent_fd,
c_oldname.as_ptr(),
newparent_fd,
c_newname.as_ptr(),
)
};
if result < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::NotFound {
return Err(FsError::NotFound.into());
}
return Err(err.into());
}
Ok(())
}
async fn statfs(&self) -> Result<FilesystemStats> {
let fd = self.root_fd.as_raw_fd();
tokio::task::spawn_blocking(move || {
let mut statfs: libc::statfs = unsafe { std::mem::zeroed() };
let result = unsafe { libc::fstatfs(fd, &mut statfs) };
if result < 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(FilesystemStats {
inodes: statfs.f_files,
bytes_used: (statfs.f_blocks - statfs.f_bfree) * statfs.f_bsize as u64,
})
})
.await
.map_err(|e| Error::Internal(e.to_string()))?
}
async fn forget(&self, ino: i64, nlookup: u64) {
if ino == ROOT_INO {
return;
}
let should_remove = {
let inodes = self.inodes.read().unwrap();
if let Some(inode) = inodes.get(&ino) {
let old = inode.nlookup.fetch_sub(nlookup, Ordering::Relaxed);
old <= nlookup
} else {
false
}
};
if should_remove {
let mut inodes = self.inodes.write().unwrap();
if let Some(inode) = inodes.remove(&ino) {
let mut src_map = self.src_to_ino.write().unwrap();
src_map.remove(&SrcId {
ino: inode.src_ino,
dev: inode.src_dev,
});
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DEFAULT_FILE_MODE;
use tempfile::tempdir;
#[tokio::test]
async fn test_hostfs_basic() -> Result<()> {
let dir = tempdir()?;
let fs = HostFS::new(dir.path())?;
let (_, file) = fs
.create_file(ROOT_INO, "test.txt", DEFAULT_FILE_MODE, 0, 0)
.await?;
file.pwrite(0, b"hello world").await?;
let stats = fs.lookup(ROOT_INO, "test.txt").await?.unwrap();
assert!(stats.is_file());
let file = fs.open(stats.ino, libc::O_RDONLY).await?;
let data = file.pread(0, 100).await?;
assert_eq!(data, b"hello world");
Ok(())
}
#[tokio::test]
async fn test_hostfs_mkdir_readdir() -> Result<()> {
let dir = tempdir()?;
let fs = HostFS::new(dir.path())?;
let subdir_stats = fs.mkdir(ROOT_INO, "subdir", 0o755, 0, 0).await?;
assert!(subdir_stats.is_directory());
let (_, file_a) = fs
.create_file(subdir_stats.ino, "a.txt", DEFAULT_FILE_MODE, 0, 0)
.await?;
file_a.pwrite(0, b"a").await?;
let (_, file_b) = fs
.create_file(subdir_stats.ino, "b.txt", DEFAULT_FILE_MODE, 0, 0)
.await?;
file_b.pwrite(0, b"b").await?;
let entries = fs.readdir(subdir_stats.ino).await?.unwrap();
assert_eq!(entries, vec!["a.txt", "b.txt"]);
Ok(())
}
#[tokio::test]
async fn test_hostfs_symlink() -> Result<()> {
let dir = tempdir()?;
let fs = HostFS::new(dir.path())?;
let (_file_stats, file) = fs
.create_file(ROOT_INO, "target.txt", DEFAULT_FILE_MODE, 0, 0)
.await?;
file.pwrite(0, b"content").await?;
let link_stats = fs.symlink(ROOT_INO, "link.txt", "target.txt", 0, 0).await?;
assert!(link_stats.is_symlink());
let target = fs.readlink(link_stats.ino).await?.unwrap();
assert_eq!(target, "target.txt");
Ok(())
}
}