Add fuser implementation
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1250,10 +1250,12 @@ dependencies = [
|
||||
"axum-extra",
|
||||
"clap",
|
||||
"fuser",
|
||||
"libc",
|
||||
"log",
|
||||
"log4rs",
|
||||
"non-empty-string",
|
||||
"openidconnect",
|
||||
"parking_lot",
|
||||
"passwords",
|
||||
"redis 0.31.0",
|
||||
"redis-macros",
|
||||
|
@@ -21,10 +21,12 @@ axum = { version = "0.8.4", features = ["macros"] }
|
||||
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||
clap = { version = "4.5.39", features = ["derive"] }
|
||||
fuser = "0.15.1"
|
||||
libc = "0.2.172"
|
||||
log = "0.4.27"
|
||||
log4rs = "1.3.0"
|
||||
non-empty-string = { version = "0.2.6", features = ["serde"] }
|
||||
openidconnect = { version = "4.0.0", features = ["reqwest"] }
|
||||
parking_lot = "0.12.4"
|
||||
passwords = "3.1.16"
|
||||
redis = { version = "0.31.0", features = ["tokio-comp"] }
|
||||
redis-macros = "0.5.4"
|
||||
|
21
flake.nix
21
flake.nix
@@ -33,6 +33,25 @@
|
||||
treefmt = inputs.treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
|
||||
in
|
||||
{
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "glyph";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
SQLX_OFFLINE = true;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
fuse3
|
||||
];
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
cargo
|
||||
@@ -42,6 +61,8 @@
|
||||
cargo-udeps
|
||||
cargo-outdated
|
||||
sqlx-cli
|
||||
fuse3
|
||||
pkg-config
|
||||
];
|
||||
};
|
||||
|
||||
|
@@ -37,8 +37,9 @@ pub struct OAuthConfig {
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct AutheliaConfig {
|
||||
pub user_database: PathBuf,
|
||||
pub struct FuseConfig {
|
||||
pub mount_directory: PathBuf,
|
||||
pub user_database_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
@@ -62,7 +63,7 @@ pub struct RedisConfig {
|
||||
pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
pub oauth: OAuthConfig,
|
||||
pub authelia: AutheliaConfig,
|
||||
pub fuse: FuseConfig,
|
||||
pub postgresql: PostgresqlConfig,
|
||||
pub redis: RedisConfig,
|
||||
}
|
||||
|
785
src/fuser.rs
Normal file
785
src/fuser.rs
Normal file
@@ -0,0 +1,785 @@
|
||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
||||
#![allow(clippy::missing_docs_in_private_items)]
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
collections::HashMap,
|
||||
ffi::CString,
|
||||
mem::MaybeUninit,
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use fuser::Filesystem;
|
||||
use libc::{
|
||||
EACCES, EINVAL, EISDIR, ENOENT, ENOSYS, EPERM, O_ACCMODE, O_APPEND, O_RDONLY, O_TRUNC,
|
||||
O_WRONLY, R_OK, W_OK, X_OK, gid_t, uid_t,
|
||||
};
|
||||
use parking_lot::{RwLock, RwLockWriteGuard};
|
||||
|
||||
use crate::config::FuseConfig;
|
||||
|
||||
struct Handle {
|
||||
ino: u64,
|
||||
flags: i32,
|
||||
cursor: i64,
|
||||
}
|
||||
|
||||
struct VariableFileState {
|
||||
contents: String,
|
||||
atime: SystemTime,
|
||||
mtime: SystemTime,
|
||||
}
|
||||
|
||||
struct StaticFileState {
|
||||
crtime: SystemTime,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
blksize: u32,
|
||||
}
|
||||
|
||||
struct Handles {
|
||||
handles: HashMap<u64, Handle>,
|
||||
next_handle: u64,
|
||||
}
|
||||
|
||||
impl Handles {
|
||||
const fn next_handle(&mut self) -> u64 {
|
||||
let handle = self.next_handle;
|
||||
self.next_handle += 1;
|
||||
handle
|
||||
}
|
||||
}
|
||||
|
||||
type WriteCallback = Box<dyn Fn(&str) + Send + Sync>;
|
||||
|
||||
pub struct AutheliaFS {
|
||||
config: FuseConfig,
|
||||
write_callback: Option<WriteCallback>,
|
||||
variable_file_state: Arc<RwLock<VariableFileState>>,
|
||||
static_file_state: Arc<StaticFileState>,
|
||||
handles: Arc<RwLock<Handles>>,
|
||||
}
|
||||
|
||||
const TTL: Duration = Duration::from_secs(1);
|
||||
const ROOT_INODE: u64 = 1;
|
||||
const PARENT_INODE: u64 = 2;
|
||||
const FILE_INODE: u64 = 3;
|
||||
|
||||
const ROOT_MODE: u16 = 0o550; // dr-xr-x---
|
||||
const FILE_MODE: u16 = 0o640; // -rw-r-----
|
||||
|
||||
pub fn stat(path: &str) -> std::io::Result<libc::stat> {
|
||||
let c_path = CString::new(path).unwrap();
|
||||
let mut stat_buf = MaybeUninit::<libc::stat>::uninit();
|
||||
let ret = unsafe { libc::stat(c_path.as_ptr(), stat_buf.as_mut_ptr()) };
|
||||
|
||||
if ret == 0 {
|
||||
Ok(unsafe { stat_buf.assume_init() })
|
||||
} else {
|
||||
Err(std::io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getuid() -> uid_t {
|
||||
unsafe { libc::getuid() }
|
||||
}
|
||||
|
||||
pub fn getgid() -> gid_t {
|
||||
unsafe { libc::getgid() }
|
||||
}
|
||||
|
||||
impl AutheliaFS {
|
||||
pub fn new(config: FuseConfig, write_callback: Option<WriteCallback>) -> Self {
|
||||
let contents = String::new();
|
||||
let time = SystemTime::now();
|
||||
let stat = stat(config.mount_directory.to_str().unwrap()).unwrap();
|
||||
let uid = getuid();
|
||||
let gid = getgid();
|
||||
|
||||
let variable_file_state = Arc::new(RwLock::new(VariableFileState {
|
||||
contents,
|
||||
atime: time,
|
||||
mtime: time,
|
||||
}));
|
||||
|
||||
let static_file_state = Arc::new(StaticFileState {
|
||||
crtime: time,
|
||||
uid,
|
||||
gid,
|
||||
blksize: u32::try_from(stat.st_blksize).unwrap_or(4096),
|
||||
});
|
||||
|
||||
let handles = Arc::new(RwLock::new(Handles {
|
||||
handles: HashMap::new(),
|
||||
next_handle: 1,
|
||||
}));
|
||||
|
||||
Self {
|
||||
config,
|
||||
write_callback,
|
||||
variable_file_state,
|
||||
static_file_state,
|
||||
handles,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount(self) -> std::io::Result<()> {
|
||||
let options = vec![];
|
||||
let mountpoint = self.config.mount_directory.clone();
|
||||
fuser::mount2(self, mountpoint, &options)
|
||||
}
|
||||
|
||||
fn get_root_attr(&self) -> fuser::FileAttr {
|
||||
fuser::FileAttr {
|
||||
ino: ROOT_INODE,
|
||||
size: self.static_file_state.blksize.into(),
|
||||
blocks: 1,
|
||||
atime: self.static_file_state.crtime,
|
||||
mtime: self.static_file_state.crtime,
|
||||
ctime: self.static_file_state.crtime,
|
||||
crtime: self.static_file_state.crtime,
|
||||
kind: fuser::FileType::Directory,
|
||||
perm: ROOT_MODE,
|
||||
nlink: 2,
|
||||
uid: self.static_file_state.uid,
|
||||
gid: self.static_file_state.gid,
|
||||
rdev: 0,
|
||||
blksize: self.static_file_state.blksize,
|
||||
flags: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_attr<T>(&self, lock: &T) -> fuser::FileAttr
|
||||
where
|
||||
T: Deref<Target = VariableFileState>,
|
||||
{
|
||||
let contents = lock.contents.as_bytes();
|
||||
|
||||
let contents_len = contents.len() as u64;
|
||||
let blocks = contents_len.div_ceil(u64::from(self.static_file_state.blksize));
|
||||
|
||||
fuser::FileAttr {
|
||||
ino: FILE_INODE,
|
||||
size: contents_len,
|
||||
blocks,
|
||||
atime: lock.atime,
|
||||
mtime: lock.mtime,
|
||||
ctime: self.static_file_state.crtime,
|
||||
crtime: self.static_file_state.crtime,
|
||||
kind: fuser::FileType::RegularFile,
|
||||
perm: FILE_MODE,
|
||||
nlink: 1,
|
||||
uid: self.static_file_state.uid,
|
||||
gid: self.static_file_state.gid,
|
||||
rdev: 0,
|
||||
blksize: self.static_file_state.blksize,
|
||||
flags: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Filesystem for AutheliaFS {
|
||||
fn lookup(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
parent: u64,
|
||||
name: &std::ffi::OsStr,
|
||||
reply: fuser::ReplyEntry,
|
||||
) {
|
||||
if parent != ROOT_INODE || *name != *self.config.user_database_name {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let attr = self.get_file_attr(&self.variable_file_state.read());
|
||||
reply.entry(&TTL, &attr, 0);
|
||||
}
|
||||
|
||||
fn getattr(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
_fh: Option<u64>,
|
||||
reply: fuser::ReplyAttr,
|
||||
) {
|
||||
if ino != ROOT_INODE && ino != FILE_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let attr = if ino == ROOT_INODE {
|
||||
self.get_root_attr()
|
||||
} else {
|
||||
let lock = self.variable_file_state.read();
|
||||
self.get_file_attr(&lock)
|
||||
};
|
||||
|
||||
reply.attr(&TTL, &attr);
|
||||
}
|
||||
|
||||
#[allow(clippy::similar_names)]
|
||||
fn setattr(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
mode: Option<u32>,
|
||||
uid: Option<u32>,
|
||||
gid: Option<u32>,
|
||||
size: Option<u64>,
|
||||
atime: Option<fuser::TimeOrNow>,
|
||||
mtime: Option<fuser::TimeOrNow>,
|
||||
ctime: Option<SystemTime>,
|
||||
fh: Option<u64>,
|
||||
crtime: Option<SystemTime>,
|
||||
_chgtime: Option<SystemTime>,
|
||||
bkuptime: Option<SystemTime>,
|
||||
_flags: Option<u32>,
|
||||
reply: fuser::ReplyAttr,
|
||||
) {
|
||||
if let Some(fh) = fh {
|
||||
let handles = self.handles.read();
|
||||
if !handles.handles.contains_key(&fh) {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ino == ROOT_INODE {
|
||||
reply.error(EPERM);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino != FILE_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
if mode.is_some() || uid.is_some() || gid.is_some() {
|
||||
reply.error(EPERM);
|
||||
return;
|
||||
}
|
||||
|
||||
if ctime.is_some() || crtime.is_some() || bkuptime.is_some() {
|
||||
reply.error(ENOSYS);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(size) = size {
|
||||
if size == 0 {
|
||||
let mut variable_file_state = self.variable_file_state.write();
|
||||
variable_file_state.contents.clear();
|
||||
} else {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if mtime.is_some() || atime.is_some() {
|
||||
let mut variable_file_state = self.variable_file_state.write();
|
||||
|
||||
variable_file_state.mtime = match mtime {
|
||||
Some(fuser::TimeOrNow::Now) => SystemTime::now(),
|
||||
Some(fuser::TimeOrNow::SpecificTime(time)) => time,
|
||||
None => variable_file_state.mtime,
|
||||
};
|
||||
|
||||
variable_file_state.atime = match atime {
|
||||
Some(fuser::TimeOrNow::Now) => SystemTime::now(),
|
||||
Some(fuser::TimeOrNow::SpecificTime(time)) => time,
|
||||
None => variable_file_state.atime,
|
||||
};
|
||||
}
|
||||
|
||||
let attr = self.get_file_attr(&self.variable_file_state.read());
|
||||
|
||||
reply.attr(&TTL, &attr);
|
||||
}
|
||||
|
||||
fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
|
||||
if ino == ROOT_INODE {
|
||||
reply.error(EISDIR);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino != FILE_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut handles = self.handles.write();
|
||||
let handle = handles.next_handle();
|
||||
|
||||
handles.handles.insert(
|
||||
handle,
|
||||
Handle {
|
||||
ino,
|
||||
flags,
|
||||
cursor: 0,
|
||||
},
|
||||
);
|
||||
|
||||
drop(handles);
|
||||
|
||||
if flags & O_TRUNC != 0 && flags & O_ACCMODE != O_RDONLY {
|
||||
let mut variable_file_state = self.variable_file_state.write();
|
||||
variable_file_state.contents.clear();
|
||||
}
|
||||
|
||||
reply.opened(handle, 0);
|
||||
}
|
||||
|
||||
fn read(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
offset: i64,
|
||||
size: u32,
|
||||
_flags: i32,
|
||||
_lock_owner: Option<u64>,
|
||||
reply: fuser::ReplyData,
|
||||
) {
|
||||
if ino == ROOT_INODE {
|
||||
reply.error(EISDIR);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino != FILE_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let handles = self.handles.read();
|
||||
if !handles.handles.contains_key(&fh) {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
let flags = handles.handles.get(&fh).unwrap().flags;
|
||||
drop(handles);
|
||||
|
||||
if flags & O_ACCMODE == O_WRONLY {
|
||||
reply.error(EPERM);
|
||||
return;
|
||||
}
|
||||
|
||||
#[allow(clippy::significant_drop_tightening)]
|
||||
let mut variable_file_state = self.variable_file_state.write();
|
||||
variable_file_state.atime = SystemTime::now();
|
||||
|
||||
#[allow(clippy::significant_drop_tightening)]
|
||||
let variable_file_state = RwLockWriteGuard::downgrade(variable_file_state);
|
||||
let contents = variable_file_state.contents.as_bytes();
|
||||
|
||||
let contents_len = i64::try_from(contents.len()).unwrap();
|
||||
|
||||
if offset < 0 || offset >= contents_len {
|
||||
reply.data(&[]);
|
||||
return;
|
||||
}
|
||||
|
||||
let end = (offset + i64::from(size)).min(contents_len);
|
||||
let data = &contents[usize::try_from(offset).unwrap()..usize::try_from(end).unwrap()];
|
||||
|
||||
reply.data(data);
|
||||
}
|
||||
|
||||
fn write(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
offset: i64,
|
||||
data: &[u8],
|
||||
_write_flags: u32,
|
||||
_flags: i32,
|
||||
_lock_owner: Option<u64>,
|
||||
reply: fuser::ReplyWrite,
|
||||
) {
|
||||
if ino == ROOT_INODE {
|
||||
reply.error(EISDIR);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino != FILE_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut handles = self.handles.write();
|
||||
|
||||
let handle = handles.handles.get_mut(&fh);
|
||||
if handle.is_none() {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let handle = handle.unwrap();
|
||||
if handle.flags & O_ACCMODE == O_RDONLY {
|
||||
reply.error(EPERM);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut variable_file_state = self.variable_file_state.write();
|
||||
|
||||
let old_end = variable_file_state.contents.len();
|
||||
|
||||
let offset = if handle.flags & O_APPEND != 0 {
|
||||
handle.cursor = i64::try_from(old_end).unwrap();
|
||||
old_end
|
||||
} else {
|
||||
if offset < 0 || offset > i64::try_from(old_end).unwrap() {
|
||||
reply.error(EINVAL);
|
||||
return;
|
||||
}
|
||||
usize::try_from(offset).unwrap()
|
||||
};
|
||||
|
||||
variable_file_state.atime = SystemTime::now();
|
||||
variable_file_state.mtime = SystemTime::now();
|
||||
|
||||
let Ok(new_data) = std::str::from_utf8(data) else {
|
||||
reply.error(EINVAL);
|
||||
return;
|
||||
};
|
||||
|
||||
let new_end = offset + new_data.len();
|
||||
let new_real_end = cmp::max(new_end, old_end);
|
||||
|
||||
let mut new_contents = String::with_capacity(new_real_end);
|
||||
new_contents.push_str(&variable_file_state.contents[..offset]);
|
||||
new_contents.push_str(new_data);
|
||||
if new_end < old_end {
|
||||
new_contents.push_str(&variable_file_state.contents[new_end..]);
|
||||
}
|
||||
variable_file_state.contents = new_contents;
|
||||
|
||||
handle.cursor = i64::try_from(offset + new_data.len()).unwrap();
|
||||
drop(handles);
|
||||
|
||||
if let Some(callback) = &self.write_callback {
|
||||
callback(&variable_file_state.contents);
|
||||
}
|
||||
|
||||
drop(variable_file_state);
|
||||
|
||||
reply.written(u32::try_from(data.len()).unwrap());
|
||||
}
|
||||
|
||||
fn flush(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
_lock_owner: u64,
|
||||
reply: fuser::ReplyEmpty,
|
||||
) {
|
||||
if ino != FILE_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.handles.read().handles.contains_key(&fh) {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn release(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
_flags: i32,
|
||||
_lock_owner: Option<u64>,
|
||||
_flush: bool,
|
||||
reply: fuser::ReplyEmpty,
|
||||
) {
|
||||
if ino != FILE_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let handle = self.handles.write().handles.remove(&fh);
|
||||
if handle.is_none() {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn fsync(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
_datasync: bool,
|
||||
reply: fuser::ReplyEmpty,
|
||||
) {
|
||||
if !self.handles.read().handles.contains_key(&fh) {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino != FILE_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn opendir(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
_flags: i32,
|
||||
reply: fuser::ReplyOpen,
|
||||
) {
|
||||
if ino == FILE_INODE {
|
||||
reply.error(EINVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino != ROOT_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut handles = self.handles.write();
|
||||
|
||||
let handle = handles.next_handle();
|
||||
handles.handles.insert(
|
||||
handle,
|
||||
Handle {
|
||||
ino,
|
||||
flags: O_RDONLY,
|
||||
cursor: 0,
|
||||
},
|
||||
);
|
||||
|
||||
drop(handles);
|
||||
|
||||
reply.opened(handle, 0);
|
||||
}
|
||||
|
||||
fn readdir(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
offset: i64,
|
||||
mut reply: fuser::ReplyDirectory,
|
||||
) {
|
||||
if !self.handles.read().handles.contains_key(&fh) {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino == FILE_INODE {
|
||||
reply.error(EINVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino != ROOT_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
if offset < 0 {
|
||||
reply.error(EINVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
let entries = [
|
||||
(ROOT_INODE, 1, fuser::FileType::Directory, "."),
|
||||
(PARENT_INODE, 2, fuser::FileType::Directory, ".."),
|
||||
(
|
||||
FILE_INODE,
|
||||
3,
|
||||
fuser::FileType::RegularFile,
|
||||
&self.config.user_database_name,
|
||||
),
|
||||
];
|
||||
|
||||
let start = usize::try_from(offset).unwrap();
|
||||
for entry in entries.iter().skip(start) {
|
||||
if reply.add(entry.0, entry.1, entry.2, entry.3) {
|
||||
// Buffer full
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn readdirplus(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
offset: i64,
|
||||
mut reply: fuser::ReplyDirectoryPlus,
|
||||
) {
|
||||
if !self.handles.read().handles.contains_key(&fh) {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino == FILE_INODE {
|
||||
reply.error(EINVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
if ino != ROOT_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
if offset < 0 {
|
||||
reply.error(EINVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
let entries = vec![
|
||||
(ROOT_INODE, 1, ".", TTL, self.get_root_attr(), 0),
|
||||
(PARENT_INODE, 2, "..", TTL, self.get_root_attr(), 0),
|
||||
(
|
||||
FILE_INODE,
|
||||
3,
|
||||
&self.config.user_database_name,
|
||||
TTL,
|
||||
self.get_file_attr(&self.variable_file_state.read()),
|
||||
0,
|
||||
),
|
||||
];
|
||||
|
||||
let start = usize::try_from(offset).unwrap();
|
||||
for entry in entries.iter().skip(start) {
|
||||
if reply.add(entry.0, entry.1, entry.2, &entry.3, &entry.4, entry.5) {
|
||||
// Buffer full
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn releasedir(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
_flags: i32,
|
||||
reply: fuser::ReplyEmpty,
|
||||
) {
|
||||
if ino != ROOT_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let handle = self.handles.write().handles.remove(&fh);
|
||||
if handle.is_none() {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn fsyncdir(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: u64,
|
||||
_datasync: bool,
|
||||
reply: fuser::ReplyEmpty,
|
||||
) {
|
||||
let handles = self.handles.read();
|
||||
if !handles.handles.contains_key(&fh) {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
drop(handles);
|
||||
|
||||
if ino != ROOT_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn statfs(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyStatfs) {
|
||||
if ino != ROOT_INODE && ino != FILE_INODE {
|
||||
reply.error(ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let variable_file_state = self.variable_file_state.read();
|
||||
let blocks = (variable_file_state.contents.len() as u64)
|
||||
.div_ceil(u64::from(self.static_file_state.blksize));
|
||||
drop(variable_file_state);
|
||||
|
||||
reply.statfs(
|
||||
blocks,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
self.static_file_state.blksize,
|
||||
u8::MAX.into(),
|
||||
u32::try_from(blocks).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
fn access(&mut self, req: &fuser::Request<'_>, ino: u64, mask: i32, reply: fuser::ReplyEmpty) {
|
||||
if ino != ROOT_INODE && ino != FILE_INODE {
|
||||
reply.error(libc::ENOENT);
|
||||
return;
|
||||
}
|
||||
|
||||
let uid = req.uid();
|
||||
let gid = req.gid();
|
||||
|
||||
#[allow(clippy::similar_names)]
|
||||
let owner_uid = self.static_file_state.uid;
|
||||
#[allow(clippy::similar_names)]
|
||||
let owner_gid = self.static_file_state.gid;
|
||||
|
||||
let mode = if ino == ROOT_INODE {
|
||||
ROOT_MODE
|
||||
} else {
|
||||
FILE_MODE
|
||||
};
|
||||
|
||||
let wants_read = (mask & R_OK) != 0;
|
||||
let wants_write = (mask & W_OK) != 0;
|
||||
let wants_exec = (mask & X_OK) != 0;
|
||||
|
||||
let perm_bits: u8 = if uid == owner_uid {
|
||||
((mode >> 6) & 0b111) as u8
|
||||
} else if gid == owner_gid {
|
||||
((mode >> 3) & 0b111) as u8
|
||||
} else {
|
||||
(mode & 0b111) as u8
|
||||
};
|
||||
|
||||
if wants_read && (perm_bits & 0b100) == 0 {
|
||||
reply.error(EACCES);
|
||||
return;
|
||||
}
|
||||
if wants_write && (perm_bits & 0b010) == 0 {
|
||||
reply.error(EACCES);
|
||||
return;
|
||||
}
|
||||
if wants_exec && (perm_bits & 0b001) == 0 {
|
||||
reply.error(EACCES);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.ok();
|
||||
}
|
||||
}
|
15
src/main.rs
15
src/main.rs
@@ -2,6 +2,7 @@
|
||||
#![allow(clippy::missing_docs_in_private_items)]
|
||||
|
||||
mod config;
|
||||
mod fuser;
|
||||
mod models;
|
||||
mod routes;
|
||||
mod state;
|
||||
@@ -25,7 +26,10 @@ async fn main() {
|
||||
let config = Config::try_from(&args.config).unwrap();
|
||||
let state = State::from_config(config.clone()).await;
|
||||
|
||||
init(&state).await.unwrap();
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&state.pg_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let routes = routes::routes(state);
|
||||
let app = axum::Router::new().nest(&format!("{}/api", config.server.subpath), routes);
|
||||
@@ -36,12 +40,3 @@ async fn main() {
|
||||
info!("Listening on {}", listener.local_addr().unwrap());
|
||||
serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn init(state: &State) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&state.pg_pool)
|
||||
.await
|
||||
.expect("Failed to run migrations");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ FROM docker.io/library/rust AS builder
|
||||
|
||||
ARG BUILD_MODE=debug
|
||||
|
||||
RUN apt-get update && apt-get clean
|
||||
RUN apt-get update && apt-get install -y fuse3 libfuse3-dev && apt-get clean
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
Reference in New Issue
Block a user