Skip to content

Commit

Permalink
feat: wip
Browse files Browse the repository at this point in the history
veeso committed Oct 21, 2024
1 parent 25eb364 commit 03dcac0
Showing 10 changed files with 281 additions and 32 deletions.
1 change: 1 addition & 0 deletions remotefs-fuse-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ remotefs-smb = { version = "0.3", optional = true }
remotefs-ssh = { version = "0.4", optional = true }
remotefs-webdav = { version = "0.2", optional = true }
thiserror = "^1"
tempfile = "3"
tokio = { version = "1", features = ["rt"] }

[features]
4 changes: 3 additions & 1 deletion remotefs-fuse-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
mod cli;

use remotefs_fuse::{Driver, MountOption};
use tempfile::TempDir;

fn main() -> anyhow::Result<()> {
env_logger::init();

let args = argh::from_env::<cli::CliArgs>();
let mount_path = args.to.clone();
let remote = args.remote();
let data_dir = TempDir::new()?;

let driver = Driver::from(remote);
let driver = Driver::new(data_dir.path(), remote)?;

// setup signal handler
ctrlc::set_handler(move || {
4 changes: 4 additions & 0 deletions remotefs-fuse/Cargo.toml
Original file line number Diff line number Diff line change
@@ -24,6 +24,10 @@ thiserror = "^1"
[target.'cfg(target_family = "unix")'.dependencies]
fuser = "0.14"
libc = "^0.2"
seahash = "4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "^1"

[dev-dependencies]
env_logger = "^0.11"
28 changes: 20 additions & 8 deletions remotefs-fuse/src/driver.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
mod error;
#[cfg(target_family = "unix")]
#[cfg_attr(docsrs, doc(cfg(target_family = "unix")))]
mod unix;

use std::path::{Path, PathBuf};

use remotefs::RemoteFs;

pub use self::error::{DriverError, DriverResult};

/// Remote Filesystem Driver
///
/// This driver takes a instance which implements the [`RemoteFs`] trait and mounts it to a local directory.
///
/// The driver will use the [`fuser`](https://crates.io/crates/fuser) crate to mount the filesystem, on Unix systems, while
/// it will use [dokan](https://crates.io/crates/dokan) on Windows.
pub struct Driver {
data_dir: PathBuf,
#[cfg(target_family = "unix")]
database: unix::InodeDb,
remote: Box<dyn RemoteFs>,
}

impl From<Box<dyn RemoteFs>> for Driver {
fn from(remote: Box<dyn RemoteFs>) -> Self {
Self::new(remote)
}
}

impl Driver {
/// Create a new instance of the [`Driver`] providing a instance which implements the [`RemoteFs`] trait.
///
/// The [`RemoteFs`] instance must be boxed.
pub fn new(remote: Box<dyn RemoteFs>) -> Self {
Self { remote }
///
/// # Arguments
///
/// * `data_dir` - A directory where inodes will be mapped.
/// * `remote` - The instance which implements the [`RemoteFs`] trait.
pub fn new(data_dir: &Path, remote: Box<dyn RemoteFs>) -> DriverResult<Self> {
Ok(Self {
data_dir: data_dir.to_path_buf(),
#[cfg(target_family = "unix")]
database: unix::InodeDb::load(&data_dir.join("inodes.json"))?,
remote,
})
}
}
10 changes: 10 additions & 0 deletions remotefs-fuse/src/driver/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use thiserror::Error;

pub type DriverResult<T> = Result<T, DriverError>;

#[derive(Debug, Error)]
pub enum DriverError {
#[cfg(target_family = "unix")]
#[error("Inode DB error: {0}")]
Inode(#[from] super::unix::InodeDbError),
}
179 changes: 162 additions & 17 deletions remotefs-fuse/src/driver/unix.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,104 @@
mod inode;

use std::ffi::OsStr;
use std::time::SystemTime;
use std::hash::{Hash as _, Hasher as _};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use fuser::{
Filesystem, KernelConfig, ReplyAttr, ReplyBmap, ReplyCreate, ReplyData, ReplyDirectory,
ReplyEmpty, ReplyEntry, ReplyLock, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request,
TimeOrNow,
FileAttr, FileType, Filesystem, KernelConfig, ReplyAttr, ReplyBmap, ReplyCreate, ReplyData,
ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyLock, ReplyOpen, ReplyStatfs, ReplyWrite,
ReplyXattr, Request, TimeOrNow,
};
use libc::c_int;
use remotefs::{File, RemoteResult};

pub use self::inode::{InodeDb, InodeDbError};
use super::Driver;

const BLOCK_SIZE: usize = 512;

/// Get the inode number for a [`Path`]
fn inode(path: &Path) -> u64 {
let mut hasher = seahash::SeaHasher::new();
path.hash(&mut hasher);
hasher.finish()
}

/// Convert a [`remotefs::fs::FileType`] to a [`FileType`] from [`fuser`]
fn convert_remote_filetype(filetype: remotefs::fs::FileType) -> FileType {
match filetype {
remotefs::fs::FileType::Directory => FileType::Directory,
remotefs::fs::FileType::File => FileType::RegularFile,
remotefs::fs::FileType::Symlink => FileType::Symlink,
}
}

/// Convert a [`File`] from [`remotefs`] to a [`FileAttr`] from [`fuser`]
fn convert_file(value: &File) -> FileAttr {
FileAttr {
ino: inode(value.path()),
size: value.metadata().size,
blocks: (value.metadata().size + BLOCK_SIZE as u64 - 1) / BLOCK_SIZE as u64,
atime: value.metadata().accessed.unwrap_or(UNIX_EPOCH),
mtime: value.metadata().modified.unwrap_or(UNIX_EPOCH),
ctime: value.metadata().created.unwrap_or(UNIX_EPOCH),
crtime: UNIX_EPOCH,
kind: convert_remote_filetype(value.metadata().file_type.clone()),
perm: value
.metadata()
.mode
.map(|mode| (u32::from(mode)) as u16)
.unwrap_or(0o777),
nlink: 0,
uid: value.metadata().uid.unwrap_or(0),
gid: value.metadata().gid.unwrap_or(0),
rdev: 0,
blksize: BLOCK_SIZE as u32,
flags: 0,
}
}

/// Convert a [`TimeOrNow`] to a [`SystemTime`]
fn time_or_now(t: TimeOrNow) -> SystemTime {
match t {
TimeOrNow::SpecificTime(t) => t,
TimeOrNow::Now => SystemTime::now(),
}
}

impl Driver {
/// Get the inode for a path.
///
/// If the inode is not in the database, it will be fetched from the remote filesystem.
fn get_inode_from_path(&mut self, path: &Path) -> RemoteResult<(File, FileAttr)> {
let (file, attrs) = self.remote.stat(path).map(|file| {
let attrs = convert_file(&file);
(file, attrs)
})?;

// Save the inode to the database
if !self.database.has(attrs.ino) {
self.database.put(attrs.ino, path.to_path_buf());
}

Ok((file, attrs))
}

/// Get the inode from the inode number
fn get_inode_from_inode(&mut self, inode: u64) -> RemoteResult<(File, FileAttr)> {
let path = self
.database
.get(inode)
.ok_or_else(|| {
remotefs::RemoteError::new(remotefs::RemoteErrorType::NoSuchFileOrDirectory)
})?
.to_path_buf();

self.get_inode_from_path(&path)
}
}

impl Filesystem for Driver {
/// Initialize filesystem.
/// Called before any other filesystem method.
@@ -36,8 +125,16 @@ impl Filesystem for Driver {
}

/// Look up a directory entry by name and get its attributes.
fn lookup(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
todo!()
fn lookup(&mut self, _req: &Request, _parent: u64, name: &OsStr, reply: ReplyEntry) {
let path = PathBuf::from(name.to_string_lossy().to_string());

match self.get_inode_from_path(path.as_path()) {
Err(err) => {
error!("Failed to get file attributes: {err}");
reply.error(libc::ENOENT)
}
Ok((_, attrs)) => reply.entry(&Duration::new(0, 0), &attrs, 0),
}
}

/// Forget about an inode.
@@ -47,13 +144,20 @@ impl Filesystem for Driver {
/// each forget. The filesystem may ignore forget calls, if the inodes don't need to
/// have a limited lifetime. On unmount it is not guaranteed, that all referenced
/// inodes will receive a forget message.
fn forget(&mut self, req: &Request, ino: u64, nlookup: u64) {
todo!()
}
fn forget(&mut self, _req: &Request, _ino: u64, _nlookup: u64) {}

/// Get file attributes.
fn getattr(&mut self, req: &Request, ino: u64, reply: ReplyAttr) {
todo!()
fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
let attrs = match self.get_inode_from_inode(ino) {
Err(err) => {
error!("Failed to get file attributes: {err}");
reply.error(libc::ENOENT);
return;
}
Ok((_, attrs)) => attrs,
};

reply.attr(&Duration::new(0, 0), &attrs);
}

/// Set file attributes.
@@ -65,17 +169,58 @@ impl Filesystem for Driver {
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
_atime: Option<TimeOrNow>,
_mtime: Option<TimeOrNow>,
_ctime: Option<SystemTime>,
fh: Option<u64>,
atime: Option<TimeOrNow>,
mtime: Option<TimeOrNow>,
ctime: Option<SystemTime>,
_fh: Option<u64>,
_crtime: Option<SystemTime>,
_chgtime: Option<SystemTime>,
_bkuptime: Option<SystemTime>,
flags: Option<u32>,
_flags: Option<u32>,
reply: ReplyAttr,
) {
todo!()
let (mut file, _) = match self.get_inode_from_inode(ino) {
Ok(attrs) => attrs,
Err(err) => {
error!("Failed to get file attributes: {err}");
reply.error(libc::ENOENT);
return;
}
};

if let Some(mode) = mode {
file.metadata.mode = Some(mode.into());
}
if let Some(uid) = uid {
file.metadata.uid = Some(uid);
}
if let Some(gid) = gid {
file.metadata.gid = Some(gid);
}
if let Some(size) = size {
file.metadata.size = size;
}
if let Some(atime) = atime {
file.metadata.accessed = Some(time_or_now(atime));
}
if let Some(mtime) = mtime {
file.metadata.modified = Some(time_or_now(mtime));
}
if let Some(ctime) = ctime {
file.metadata.created = Some(ctime);
}

// set attributes
match self.remote.setstat(file.path(), file.metadata().clone()) {
Ok(_) => {
let attrs = convert_file(&file);
reply.attr(&Duration::new(0, 0), &attrs);
}
Err(err) => {
error!("Failed to set file attributes: {err}");
reply.error(libc::EIO);
}
}
}

/// Read symbolic link.
71 changes: 71 additions & 0 deletions remotefs-fuse/src/driver/unix/inode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use thiserror::Error;

/// Error type for InodeDb
#[derive(Error, Debug)]
pub enum InodeDbError {
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Serde error: {0}")]
SerdeError(#[from] serde_json::Error),
}

pub type InodeDbResult<T> = Result<T, InodeDbError>;
pub type Inode = u64;

type Database = HashMap<Inode, PathBuf>;

/// A database to map inodes to files
///
/// The database is saved to a file when the instance is dropped
#[derive(Debug, Default, Clone)]
pub struct InodeDb {
database: Database,
path: PathBuf,
}

impl InodeDb {
/// Load [`InodeDb`] from a file
pub fn load(path: &Path) -> Result<Self, InodeDbError> {
let data = std::fs::read_to_string(path)?;
let database: Database = serde_json::from_str(&data)?;

Ok(Self {
database,
path: path.to_path_buf(),
})
}

/// Check if the database contains an inode
pub fn has(&self, inode: Inode) -> bool {
self.database.contains_key(&inode)
}

/// Put a new inode into the database
pub fn put(&mut self, inode: Inode, path: PathBuf) {
self.database.insert(inode, path);
}

/// Get a path from an inode
pub fn get(&self, inode: Inode) -> Option<&Path> {
self.database.get(&inode).map(|x| x.as_path())
}

/// Save [`InodeDb`] to a file
fn save(&self) -> InodeDbResult<()> {
let data = serde_json::to_string(&self.database)?;
std::fs::write(&self.path, data)?;

Ok(())
}
}

impl Drop for InodeDb {
fn drop(&mut self) {
if let Err(err) = self.save() {
error!("Failed to save InodeDb: {err}");
}
}
}
2 changes: 1 addition & 1 deletion remotefs-fuse/src/lib.rs
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ use std::path::Path;
#[cfg(target_family = "unix")]
pub use fuser::{spawn_mount2, MountOption};

pub use self::driver::Driver;
pub use self::driver::{Driver, DriverError, DriverResult};

/// Mount a remote filesystem to a local directory.
///
Loading

0 comments on commit 03dcac0

Please sign in to comment.