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",
|
"axum-extra",
|
||||||
"clap",
|
"clap",
|
||||||
"fuser",
|
"fuser",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"log4rs",
|
"log4rs",
|
||||||
"non-empty-string",
|
"non-empty-string",
|
||||||
"openidconnect",
|
"openidconnect",
|
||||||
|
"parking_lot",
|
||||||
"passwords",
|
"passwords",
|
||||||
"redis 0.31.0",
|
"redis 0.31.0",
|
||||||
"redis-macros",
|
"redis-macros",
|
||||||
|
@@ -21,10 +21,12 @@ axum = { version = "0.8.4", features = ["macros"] }
|
|||||||
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
axum-extra = { version = "0.10.1", features = ["typed-header"] }
|
||||||
clap = { version = "4.5.39", features = ["derive"] }
|
clap = { version = "4.5.39", features = ["derive"] }
|
||||||
fuser = "0.15.1"
|
fuser = "0.15.1"
|
||||||
|
libc = "0.2.172"
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
log4rs = "1.3.0"
|
log4rs = "1.3.0"
|
||||||
non-empty-string = { version = "0.2.6", features = ["serde"] }
|
non-empty-string = { version = "0.2.6", features = ["serde"] }
|
||||||
openidconnect = { version = "4.0.0", features = ["reqwest"] }
|
openidconnect = { version = "4.0.0", features = ["reqwest"] }
|
||||||
|
parking_lot = "0.12.4"
|
||||||
passwords = "3.1.16"
|
passwords = "3.1.16"
|
||||||
redis = { version = "0.31.0", features = ["tokio-comp"] }
|
redis = { version = "0.31.0", features = ["tokio-comp"] }
|
||||||
redis-macros = "0.5.4"
|
redis-macros = "0.5.4"
|
||||||
|
21
flake.nix
21
flake.nix
@@ -33,6 +33,25 @@
|
|||||||
treefmt = inputs.treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
|
treefmt = inputs.treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
|
||||||
in
|
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 {
|
devShells.default = pkgs.mkShell {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
cargo
|
cargo
|
||||||
@@ -42,6 +61,8 @@
|
|||||||
cargo-udeps
|
cargo-udeps
|
||||||
cargo-outdated
|
cargo-outdated
|
||||||
sqlx-cli
|
sqlx-cli
|
||||||
|
fuse3
|
||||||
|
pkg-config
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -37,8 +37,9 @@ pub struct OAuthConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
pub struct AutheliaConfig {
|
pub struct FuseConfig {
|
||||||
pub user_database: PathBuf,
|
pub mount_directory: PathBuf,
|
||||||
|
pub user_database_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
@@ -62,7 +63,7 @@ pub struct RedisConfig {
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub server: ServerConfig,
|
pub server: ServerConfig,
|
||||||
pub oauth: OAuthConfig,
|
pub oauth: OAuthConfig,
|
||||||
pub authelia: AutheliaConfig,
|
pub fuse: FuseConfig,
|
||||||
pub postgresql: PostgresqlConfig,
|
pub postgresql: PostgresqlConfig,
|
||||||
pub redis: RedisConfig,
|
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)]
|
#![allow(clippy::missing_docs_in_private_items)]
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod fuser;
|
||||||
mod models;
|
mod models;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod state;
|
mod state;
|
||||||
@@ -25,7 +26,10 @@ async fn main() {
|
|||||||
let config = Config::try_from(&args.config).unwrap();
|
let config = Config::try_from(&args.config).unwrap();
|
||||||
let state = State::from_config(config.clone()).await;
|
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 routes = routes::routes(state);
|
||||||
let app = axum::Router::new().nest(&format!("{}/api", config.server.subpath), routes);
|
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());
|
info!("Listening on {}", listener.local_addr().unwrap());
|
||||||
serve(listener, app).await.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
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user