mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-03 13:54:53 +03:00
* fix(clipboard): clean up stale Linux FUSE mounts Recover Linux file clipboard FUSE mount points before remounting and stop treating a cached context as valid when the mount has already gone away. This fixes the desktop file manager copy failure that shows dialogs such as "Error while copying a" and "There was an error copying the file into xxx". Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): fuse, reduce dups Signed-off-by: fufesou <linlong1266@gmail.com> * fix: clear Linux file clipboard before unmounting FUSE Ensure Linux client teardown clears RustDesk file clipboard URLs while the FUSE context is still available. Also prefer fusermount before umount to avoid noisy unprivileged teardown attempts. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): return and log errors Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
1004 lines
29 KiB
Rust
1004 lines
29 KiB
Rust
//! fuse server implement
|
|
//! we use fuse to provide file readers, warping data transfer to file interfaces
|
|
//!
|
|
//! # Name encoding
|
|
//!
|
|
//! There are different collection of characters forbidden in file names on different platforms:
|
|
//! - windows: `\/:*?"<>|`
|
|
//! - macos: `:/`
|
|
//! - linux: `/`
|
|
//!
|
|
//! what makes it troublesome is windows also used '\' as path separator.
|
|
//!
|
|
//! For now, we transfer all file names with windows separators, UTF-16 encoded.
|
|
//! *Need a way to transfer file names with '\' safely*.
|
|
//! Maybe we can use URL encoded file names and '/' separators as a new standard, while keep the support to old schemes.
|
|
//!
|
|
//! # Note
|
|
//! - all files on FS should be read only, and mark the owner to be the current user
|
|
//! - any write operations, hard links, and symbolic links on the FS should be denied
|
|
|
|
use std::{
|
|
collections::{BTreeMap, HashMap},
|
|
ffi::OsString,
|
|
path::{Path, PathBuf},
|
|
sync::{
|
|
atomic::{AtomicBool, AtomicU64, Ordering},
|
|
mpsc::{Receiver, Sender},
|
|
Arc,
|
|
},
|
|
time::{Duration, SystemTime},
|
|
};
|
|
|
|
use fuser::{ReplyDirectory, FUSE_ROOT_ID};
|
|
use hbb_common::log;
|
|
use parking_lot::{Condvar, Mutex};
|
|
|
|
use crate::{
|
|
platform::unix::{
|
|
filetype::{FileDescription, FileType, Inode, MAX_NAME_LEN, PERM_RWX},
|
|
BLOCK_SIZE,
|
|
},
|
|
send_data, ClipboardFile, CliprdrError,
|
|
};
|
|
|
|
/// fuse server ready retry max times
|
|
const READ_RETRY: i32 = 3;
|
|
|
|
impl From<FileType> for fuser::FileType {
|
|
fn from(value: FileType) -> Self {
|
|
match value {
|
|
FileType::File => Self::RegularFile,
|
|
FileType::Directory => Self::Directory,
|
|
FileType::Symlink => Self::Symlink,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// fuse client
|
|
/// this is a proxy to the fuse server
|
|
#[derive(Debug)]
|
|
pub struct FuseClient {
|
|
server: Arc<Mutex<FuseServer>>,
|
|
}
|
|
|
|
impl fuser::Filesystem for FuseClient {
|
|
fn init(
|
|
&mut self,
|
|
req: &fuser::Request<'_>,
|
|
config: &mut fuser::KernelConfig,
|
|
) -> Result<(), libc::c_int> {
|
|
let mut server = self.server.lock();
|
|
server.init(req, config)
|
|
}
|
|
|
|
fn lookup(
|
|
&mut self,
|
|
req: &fuser::Request<'_>,
|
|
parent: u64,
|
|
name: &std::ffi::OsStr,
|
|
reply: fuser::ReplyEntry,
|
|
) {
|
|
let mut server = self.server.lock();
|
|
server.lookup(req, parent, name, reply)
|
|
}
|
|
|
|
fn opendir(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
|
|
let mut server = self.server.lock();
|
|
server.opendir(req, ino, flags, reply)
|
|
}
|
|
|
|
fn readdir(
|
|
&mut self,
|
|
req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
fh: u64,
|
|
offset: i64,
|
|
reply: fuser::ReplyDirectory,
|
|
) {
|
|
let mut server = self.server.lock();
|
|
server.readdir(req, ino, fh, offset, reply)
|
|
}
|
|
|
|
fn releasedir(
|
|
&mut self,
|
|
req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
fh: u64,
|
|
_flags: i32,
|
|
reply: fuser::ReplyEmpty,
|
|
) {
|
|
let mut server = self.server.lock();
|
|
server.releasedir(req, ino, fh, _flags, reply)
|
|
}
|
|
|
|
fn open(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
|
|
let mut server = self.server.lock();
|
|
server.open(req, ino, flags, reply)
|
|
}
|
|
|
|
fn read(
|
|
&mut self,
|
|
req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
fh: u64,
|
|
offset: i64,
|
|
size: u32,
|
|
flags: i32,
|
|
lock_owner: Option<u64>,
|
|
reply: fuser::ReplyData,
|
|
) {
|
|
let mut server = self.server.lock();
|
|
server.read(req, ino, fh, offset, size, flags, lock_owner, reply)
|
|
}
|
|
|
|
fn release(
|
|
&mut self,
|
|
req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
fh: u64,
|
|
_flags: i32,
|
|
_lock_owner: Option<u64>,
|
|
_flush: bool,
|
|
reply: fuser::ReplyEmpty,
|
|
) {
|
|
let mut server = self.server.lock();
|
|
server.release(req, ino, fh, _flags, _lock_owner, _flush, reply)
|
|
}
|
|
|
|
fn getattr(
|
|
&mut self,
|
|
req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
fh: Option<u64>,
|
|
reply: fuser::ReplyAttr,
|
|
) {
|
|
let mut server = self.server.lock();
|
|
server.getattr(req, ino, fh, reply)
|
|
}
|
|
|
|
fn statfs(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyStatfs) {
|
|
let mut server = self.server.lock();
|
|
server.statfs(req, ino, reply)
|
|
}
|
|
}
|
|
|
|
/// fuse server
|
|
/// provides a read-only file system
|
|
#[derive(Debug)]
|
|
pub(crate) struct FuseServer {
|
|
generation: AtomicU64,
|
|
files: Vec<FuseNode>,
|
|
// file handle counter
|
|
file_handle_counter: AtomicU64,
|
|
// timeout
|
|
timeout: Duration,
|
|
// file read reply channel
|
|
rx: Receiver<ClipboardFile>,
|
|
}
|
|
|
|
impl FuseServer {
|
|
/// create a new fuse server
|
|
pub fn new(timeout: Duration) -> (Self, Sender<ClipboardFile>) {
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
(
|
|
Self {
|
|
generation: AtomicU64::new(0),
|
|
files: Vec::new(),
|
|
file_handle_counter: AtomicU64::new(0),
|
|
timeout,
|
|
rx,
|
|
},
|
|
tx,
|
|
)
|
|
}
|
|
|
|
pub fn client(server: Arc<Mutex<Self>>) -> FuseClient {
|
|
FuseClient { server }
|
|
}
|
|
}
|
|
|
|
impl FuseServer {
|
|
pub fn load_file_list(&mut self, files: Vec<FileDescription>) -> Result<(), CliprdrError> {
|
|
let tree = FuseNode::build_tree(files)?;
|
|
self.files = tree;
|
|
self.generation.fetch_add(1, Ordering::Relaxed);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl fuser::Filesystem for FuseServer {
|
|
fn init(
|
|
&mut self,
|
|
_req: &fuser::Request<'_>,
|
|
_config: &mut fuser::KernelConfig,
|
|
) -> Result<(), libc::c_int> {
|
|
if self.files.is_empty() {
|
|
// create a root file
|
|
let root = FuseNode::new_root();
|
|
self.files.push(root);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn lookup(
|
|
&mut self,
|
|
_req: &fuser::Request<'_>,
|
|
parent: u64,
|
|
name: &std::ffi::OsStr,
|
|
reply: fuser::ReplyEntry,
|
|
) {
|
|
if name.len() > MAX_NAME_LEN {
|
|
log::debug!("fuse: name too long");
|
|
reply.error(libc::ENAMETOOLONG);
|
|
return;
|
|
}
|
|
|
|
let entries = &self.files;
|
|
|
|
let generation = self.generation.load(Ordering::Relaxed);
|
|
|
|
let parent_entry = match entries.get(parent as usize - 1) {
|
|
Some(f) => f,
|
|
None => {
|
|
log::error!("fuse: parent not found");
|
|
reply.error(libc::ENOENT);
|
|
return;
|
|
}
|
|
};
|
|
|
|
if parent_entry.attributes.kind != FileType::Directory {
|
|
log::error!("fuse: parent is not a directory");
|
|
reply.error(libc::ENOTDIR);
|
|
return;
|
|
}
|
|
|
|
let children_inodes = &parent_entry.children;
|
|
|
|
for inode in children_inodes.iter().copied() {
|
|
let child = &entries[inode as usize - 1];
|
|
let entry_name = OsString::from(&child.name);
|
|
|
|
if &entry_name.as_os_str() == &name {
|
|
let ttl = std::time::Duration::new(0, 0);
|
|
reply.entry(&ttl, &(&child.attributes).into(), generation);
|
|
log::debug!("fuse: found child");
|
|
return;
|
|
}
|
|
}
|
|
// error
|
|
reply.error(libc::ENOENT);
|
|
log::debug!("fuse: child not found");
|
|
}
|
|
|
|
fn opendir(
|
|
&mut self,
|
|
_req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
_flags: i32,
|
|
reply: fuser::ReplyOpen,
|
|
) {
|
|
let files = &self.files;
|
|
let Some(entry) = files.get(ino as usize - 1) else {
|
|
reply.error(libc::ENOENT);
|
|
log::error!("fuse: opendir: entry not found");
|
|
return;
|
|
};
|
|
if entry.attributes.kind != FileType::Directory {
|
|
reply.error(libc::ENOTDIR);
|
|
log::error!("fuse: opendir: entry is not a directory");
|
|
return;
|
|
}
|
|
// in gc, deny open
|
|
if entry.marked() {
|
|
log::error!("fuse: opendir: entry is in gc");
|
|
reply.error(libc::EBUSY);
|
|
return;
|
|
}
|
|
|
|
let fh = self.alloc_fd();
|
|
entry.add_handler(fh);
|
|
reply.opened(fh, 0);
|
|
}
|
|
|
|
fn readdir(
|
|
&mut self,
|
|
_req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
fh: u64,
|
|
offset: i64,
|
|
mut reply: ReplyDirectory,
|
|
) {
|
|
let files = &self.files;
|
|
let Some(entry) = files.get(ino as usize - 1) else {
|
|
reply.error(libc::ENOENT);
|
|
log::error!("fuse: readdir: entry not found");
|
|
return;
|
|
};
|
|
if !entry.have_handler(fh) {
|
|
reply.error(libc::EBADF);
|
|
log::error!("fuse: readdir: entry has no such handler");
|
|
return;
|
|
}
|
|
if entry.attributes.kind != FileType::Directory {
|
|
reply.error(libc::ENOTDIR);
|
|
log::error!("fuse: readdir: entry is not a directory");
|
|
return;
|
|
}
|
|
|
|
let offset = offset as usize;
|
|
let mut entries = Vec::new();
|
|
|
|
let self_entry = (ino, FileType::Directory, OsString::from("."));
|
|
entries.push(self_entry);
|
|
|
|
if let Some(parent_inode) = entry.parent {
|
|
entries.push((parent_inode, FileType::Directory, OsString::from("..")));
|
|
}
|
|
|
|
for inode in entry.children.iter().copied() {
|
|
let child = &files[inode as usize - 1];
|
|
let kind = child.attributes.kind;
|
|
let name = OsString::from(&child.name);
|
|
let child_entry = (inode, kind, name.to_owned());
|
|
entries.push(child_entry);
|
|
}
|
|
|
|
for (i, entry) in entries.into_iter().enumerate().skip(offset) {
|
|
if reply.add(entry.0, i as i64 + 1, entry.1.into(), entry.2) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
reply.ok();
|
|
}
|
|
|
|
fn releasedir(
|
|
&mut self,
|
|
_req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
fh: u64,
|
|
_flags: i32,
|
|
reply: fuser::ReplyEmpty,
|
|
) {
|
|
let files = &self.files;
|
|
let Some(entry) = files.get(ino as usize - 1) else {
|
|
reply.error(libc::ENOENT);
|
|
log::error!("fuse: releasedir: entry not found");
|
|
return;
|
|
};
|
|
if entry.attributes.kind != FileType::Directory {
|
|
reply.error(libc::ENOTDIR);
|
|
log::error!("fuse: releasedir: entry is not a directory");
|
|
return;
|
|
}
|
|
if !entry.have_handler(fh) {
|
|
reply.error(libc::EBADF);
|
|
log::error!("fuse: releasedir: entry has no such handler");
|
|
return;
|
|
}
|
|
|
|
let _ = entry.unregister_handler(fh);
|
|
reply.ok();
|
|
}
|
|
|
|
fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, _flags: i32, reply: fuser::ReplyOpen) {
|
|
let files = &self.files;
|
|
let Some(entry) = files.get(ino as usize - 1) else {
|
|
reply.error(libc::ENOENT);
|
|
log::error!("fuse: open: entry not found");
|
|
return;
|
|
};
|
|
|
|
// todo: support link file
|
|
if entry.attributes.kind != FileType::File {
|
|
reply.error(libc::ENFILE);
|
|
log::error!("fuse: open: entry is not a file");
|
|
return;
|
|
}
|
|
|
|
// check gc
|
|
if entry.marked() {
|
|
reply.error(libc::EBUSY);
|
|
log::error!("fuse: open: entry is in gc");
|
|
return;
|
|
}
|
|
|
|
let fh = self.alloc_fd();
|
|
entry.add_handler(fh);
|
|
reply.opened(fh, 0);
|
|
}
|
|
|
|
// todo: implement retry
|
|
fn read(
|
|
&mut self,
|
|
_req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
fh: u64,
|
|
offset: i64,
|
|
size: u32,
|
|
_flags: i32,
|
|
_lock_owner: Option<u64>,
|
|
reply: fuser::ReplyData,
|
|
) {
|
|
let files = &self.files;
|
|
let Some(entry) = files.get(ino as usize - 1) else {
|
|
reply.error(libc::ENOENT);
|
|
log::error!("fuse: read: entry not found");
|
|
return;
|
|
};
|
|
if !entry.have_handler(fh) {
|
|
reply.error(libc::EBADF);
|
|
log::error!("fuse: read: entry has no such handler");
|
|
return;
|
|
}
|
|
if entry.attributes.kind != FileType::File {
|
|
reply.error(libc::ENFILE);
|
|
log::error!("fuse: read: entry is not a file");
|
|
return;
|
|
}
|
|
|
|
if entry.marked() {
|
|
reply.error(libc::EBUSY);
|
|
log::error!("fuse: read: entry is in gc");
|
|
return;
|
|
}
|
|
|
|
let bytes = match self.read_node(entry, offset, size) {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
log::error!("failed to read entry: {:?}", e);
|
|
reply.error(libc::EIO);
|
|
return;
|
|
}
|
|
};
|
|
|
|
reply.data(bytes.as_slice());
|
|
}
|
|
|
|
fn release(
|
|
&mut self,
|
|
_req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
fh: u64,
|
|
_flags: i32,
|
|
_lock_owner: Option<u64>,
|
|
_flush: bool,
|
|
reply: fuser::ReplyEmpty,
|
|
) {
|
|
let files = &self.files;
|
|
let Some(entry) = files.get(ino as usize - 1) else {
|
|
reply.error(libc::ENOENT);
|
|
log::error!("fuse: release: entry not found");
|
|
return;
|
|
};
|
|
|
|
if entry.unregister_handler(fh).is_err() {
|
|
reply.error(libc::EBADF);
|
|
log::error!("fuse: release: entry has no such handler");
|
|
return;
|
|
}
|
|
reply.ok();
|
|
}
|
|
|
|
fn getattr(
|
|
&mut self,
|
|
_req: &fuser::Request<'_>,
|
|
ino: u64,
|
|
_fh: Option<u64>,
|
|
reply: fuser::ReplyAttr,
|
|
) {
|
|
let files = &self.files;
|
|
let Some(entry) = files.get(ino as usize - 1) else {
|
|
reply.error(libc::ENOENT);
|
|
log::error!("fuse: getattr: entry not found");
|
|
return;
|
|
};
|
|
|
|
let attr = (&entry.attributes).into();
|
|
reply.attr(&std::time::Duration::default(), &attr)
|
|
}
|
|
|
|
fn statfs(&mut self, _req: &fuser::Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) {
|
|
let mut blocks = 0;
|
|
for file in self.files.iter() {
|
|
blocks += file.attributes.size / (BLOCK_SIZE as u64)
|
|
+ (file.attributes.size % (BLOCK_SIZE as u64) != 0) as u64;
|
|
}
|
|
reply.statfs(blocks, 0, 0, 0, 0, BLOCK_SIZE, 512, BLOCK_SIZE)
|
|
}
|
|
}
|
|
|
|
impl FuseServer {
|
|
// get files and directory path right in root of FUSE fs
|
|
pub fn list_root(&self) -> Vec<PathBuf> {
|
|
let files = &self.files;
|
|
let children = &files[0].children;
|
|
let mut paths = Vec::with_capacity(children.len());
|
|
for inode in children.iter().copied() {
|
|
let idx = inode as usize - 1;
|
|
paths.push(PathBuf::from(&files[idx].name));
|
|
}
|
|
paths
|
|
}
|
|
|
|
/// allocate a new file descriptor
|
|
fn alloc_fd(&self) -> u64 {
|
|
self.file_handle_counter.fetch_add(1, Ordering::Relaxed)
|
|
}
|
|
|
|
fn read_node(
|
|
&self,
|
|
node: &FuseNode,
|
|
offset: i64,
|
|
size: u32,
|
|
) -> Result<Vec<u8>, std::io::Error> {
|
|
let request_stream_id = rand::random();
|
|
let cb_requested = unsafe {
|
|
// convert `size` from u32 to i32
|
|
// yet with same bit representation
|
|
std::mem::transmute::<u32, i32>(size)
|
|
};
|
|
|
|
let (n_position_high, n_position_low) =
|
|
((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32);
|
|
let request = ClipboardFile::FileContentsRequest {
|
|
stream_id: request_stream_id,
|
|
list_index: node.index as i32,
|
|
dw_flags: 2,
|
|
n_position_low,
|
|
n_position_high,
|
|
cb_requested,
|
|
have_clip_data_id: false,
|
|
clip_data_id: 0,
|
|
};
|
|
|
|
send_data(node.conn_id, request.clone()).map_err(|e| {
|
|
log::error!("failed to send file list to channel: {:?}", e);
|
|
std::io::Error::new(std::io::ErrorKind::Other, e)
|
|
})?;
|
|
|
|
let mut retry_times = 0;
|
|
|
|
// to-do: more tests needed
|
|
loop {
|
|
let reply = self.rx.recv_timeout(self.timeout).map_err(|e| {
|
|
log::error!("failed to receive file list from channel: {:?}", e);
|
|
std::io::Error::new(std::io::ErrorKind::TimedOut, e)
|
|
})?;
|
|
|
|
match reply {
|
|
ClipboardFile::FileContentsResponse {
|
|
msg_flags,
|
|
stream_id,
|
|
requested_data,
|
|
} => {
|
|
if stream_id != request_stream_id {
|
|
log::debug!("stream id mismatch, ignore");
|
|
continue;
|
|
}
|
|
|
|
if msg_flags & 1 == 0 {
|
|
retry_times += 1;
|
|
if retry_times > READ_RETRY {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::Other,
|
|
"failure request",
|
|
));
|
|
}
|
|
|
|
send_data(node.conn_id, request.clone()).map_err(|e| {
|
|
log::error!("failed to send file list to channel: {:?}", e);
|
|
std::io::Error::new(std::io::ErrorKind::Other, e)
|
|
})?;
|
|
continue;
|
|
}
|
|
return Ok(requested_data);
|
|
}
|
|
_ => {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::Other,
|
|
"invalid reply",
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/// a node in the FUSE file tree
|
|
#[derive(Debug)]
|
|
struct FuseNode {
|
|
/// connection id
|
|
pub conn_id: i32,
|
|
|
|
/// file index in peer's file list
|
|
/// NOTE:
|
|
/// it is NOT the same as inode, this is the index in the file list
|
|
pub index: usize,
|
|
|
|
/// parent inode
|
|
pub parent: Option<u64>,
|
|
|
|
/// file name
|
|
pub name: String,
|
|
/// file attributes
|
|
pub attributes: InodeAttributes,
|
|
/// children inodes
|
|
pub children: Vec<Inode>,
|
|
|
|
/// marked gc
|
|
pub file_handlers: FileHandles,
|
|
}
|
|
|
|
impl FuseNode {
|
|
pub fn from_description(inode: Inode, desc: FileDescription) -> Self {
|
|
Self {
|
|
conn_id: desc.conn_id,
|
|
index: inode as usize - 2,
|
|
name: desc
|
|
.name
|
|
.to_str()
|
|
.map(|s| s.to_string())
|
|
.unwrap_or_default(),
|
|
parent: None,
|
|
attributes: InodeAttributes::from_description(inode, desc),
|
|
children: Vec::new(),
|
|
file_handlers: FileHandles::new(),
|
|
}
|
|
}
|
|
|
|
pub fn new_root() -> Self {
|
|
Self {
|
|
conn_id: 0,
|
|
index: 0,
|
|
name: String::from("/"),
|
|
parent: None,
|
|
attributes: InodeAttributes::new_root(),
|
|
children: Vec::new(),
|
|
file_handlers: FileHandles::new(),
|
|
}
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn is_file(&self) -> bool {
|
|
self.attributes.kind == FileType::File
|
|
}
|
|
|
|
pub fn marked(&self) -> bool {
|
|
self.file_handlers.marked()
|
|
}
|
|
|
|
pub fn add_handler(&self, fh: u64) {
|
|
self.file_handlers.add_handler(fh)
|
|
}
|
|
|
|
pub fn unregister_handler(&self, fh: u64) -> Result<(), std::io::Error> {
|
|
self.file_handlers.unregister(fh)
|
|
}
|
|
|
|
pub fn have_handler(&self, fh: u64) -> bool {
|
|
self.file_handlers.have_handler(fh)
|
|
}
|
|
|
|
/// add a child inode
|
|
fn add_child(&mut self, inode: Inode) {
|
|
self.children.push(inode);
|
|
}
|
|
|
|
/// calculate the file tree from a pre-ordered file list
|
|
/// ## implement detail:
|
|
/// - a new root entry will be prepended to the list
|
|
/// - all file names will be trimed to the last component
|
|
pub fn build_tree(files: Vec<FileDescription>) -> Result<Vec<Self>, CliprdrError> {
|
|
// capacity set to file count + 1 (root)
|
|
let mut tree_list = Vec::with_capacity(files.len() + 1);
|
|
let root = Self::new_root();
|
|
tree_list.push(root);
|
|
|
|
// build the tree first
|
|
// root map, name -> inode
|
|
let mut sub_root_map = HashMap::new();
|
|
sub_root_map.insert(Path::new("/").to_path_buf(), FUSE_ROOT_ID);
|
|
sub_root_map.insert(Path::new("").to_path_buf(), FUSE_ROOT_ID);
|
|
|
|
for file in files.into_iter() {
|
|
let name = file.name.clone();
|
|
|
|
let inode = tree_list.len() as u64 + FUSE_ROOT_ID;
|
|
let parent_inode = match name.parent() {
|
|
Some(parent) => sub_root_map.get(parent).copied().unwrap_or(FUSE_ROOT_ID),
|
|
None => {
|
|
// parent should be root
|
|
FUSE_ROOT_ID
|
|
}
|
|
};
|
|
tree_list[parent_inode as usize - 1].add_child(inode);
|
|
|
|
let base_name = name.file_name().ok_or_else(|| {
|
|
let err = std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
format!("invalid file name {}", file.name.display()),
|
|
);
|
|
CliprdrError::FileError {
|
|
path: file.name.to_string_lossy().to_string(),
|
|
err,
|
|
}
|
|
})?;
|
|
|
|
let mut desc = file.clone();
|
|
|
|
if desc.kind == FileType::Directory {
|
|
sub_root_map.insert(desc.name.clone(), inode);
|
|
}
|
|
|
|
desc.name = Path::new(base_name).to_path_buf();
|
|
|
|
let mut fuse_node = FuseNode::from_description(inode, desc);
|
|
fuse_node.parent = Some(parent_inode);
|
|
tree_list.push(fuse_node);
|
|
}
|
|
Ok(tree_list)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct InodeAttributes {
|
|
inode: Inode,
|
|
size: u64,
|
|
// file reference meta
|
|
// should be the only mutable field in this struct
|
|
last_accessed: std::time::SystemTime,
|
|
last_modified: std::time::SystemTime,
|
|
last_metadata_changed: std::time::SystemTime,
|
|
creation_time: std::time::SystemTime,
|
|
kind: FileType,
|
|
perm: u16,
|
|
|
|
// not implemented
|
|
_xattrs: BTreeMap<Vec<u8>, Vec<u8>>,
|
|
}
|
|
|
|
impl InodeAttributes {
|
|
pub fn new(inode: u64, size: u64, perm: u16, kind: FileType) -> Self {
|
|
Self {
|
|
inode,
|
|
size,
|
|
last_accessed: std::time::SystemTime::now(),
|
|
last_modified: std::time::SystemTime::now(),
|
|
last_metadata_changed: std::time::SystemTime::now(),
|
|
creation_time: std::time::SystemTime::now(),
|
|
kind,
|
|
perm,
|
|
_xattrs: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn from_description(inode: u64, desc: FileDescription) -> Self {
|
|
Self {
|
|
inode,
|
|
size: desc.size,
|
|
last_modified: desc.last_modified,
|
|
last_metadata_changed: desc.last_metadata_changed,
|
|
creation_time: desc.creation_time,
|
|
last_accessed: SystemTime::now(),
|
|
kind: desc.kind,
|
|
perm: desc.perm,
|
|
|
|
_xattrs: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn new_root() -> Self {
|
|
Self::new(FUSE_ROOT_ID, 0, PERM_RWX, FileType::Directory)
|
|
}
|
|
|
|
pub fn access(&mut self) {
|
|
self.last_accessed = std::time::SystemTime::now();
|
|
}
|
|
}
|
|
|
|
impl From<&InodeAttributes> for fuser::FileAttr {
|
|
fn from(value: &InodeAttributes) -> Self {
|
|
let blocks = if value.size % BLOCK_SIZE as u64 == 0 {
|
|
value.size / BLOCK_SIZE as u64
|
|
} else {
|
|
value.size / BLOCK_SIZE as u64 + 1
|
|
};
|
|
Self {
|
|
ino: value.inode,
|
|
size: value.size,
|
|
blocks,
|
|
atime: value.last_accessed,
|
|
mtime: value.last_modified,
|
|
ctime: value.last_metadata_changed,
|
|
crtime: value.creation_time,
|
|
kind: value.kind.into(),
|
|
|
|
// read only
|
|
perm: value.perm,
|
|
|
|
nlink: 1,
|
|
// set to current user
|
|
uid: unsafe { libc::getuid() },
|
|
// set to current user
|
|
gid: unsafe { libc::getgid() },
|
|
rdev: 0,
|
|
blksize: BLOCK_SIZE,
|
|
// todo: support macos flags
|
|
flags: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct FileHandles {
|
|
waiter: Condvar,
|
|
handlers: Mutex<Vec<u64>>,
|
|
gc: AtomicBool,
|
|
}
|
|
|
|
impl FileHandles {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
waiter: Condvar::new(),
|
|
// the vector in handlers is sorted, from small to big
|
|
// prove:
|
|
// - later allocated handler will be bigger than previous ones
|
|
// - new handlers will append to the end of the vector
|
|
// - dropping old handlers won't affect the ordering
|
|
handlers: Mutex::new(Vec::new()),
|
|
gc: AtomicBool::new(false),
|
|
}
|
|
}
|
|
|
|
pub fn add_handler(&self, fh: u64) {
|
|
if self.marked() {
|
|
panic!("adding new handler to a marked ref counter");
|
|
}
|
|
self.handlers.lock().push(fh);
|
|
}
|
|
|
|
pub fn marked(&self) -> bool {
|
|
self.gc.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn have_handler(&self, handler: u64) -> bool {
|
|
let handlers = self.handlers.lock();
|
|
handlers.binary_search(&handler).is_ok()
|
|
}
|
|
|
|
pub fn unregister(&self, handler: u64) -> Result<(), std::io::Error> {
|
|
let mut handlers = self.handlers.lock();
|
|
|
|
let Ok(idx) = handlers.binary_search(&handler) else {
|
|
let e = std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid handler");
|
|
return Err(e);
|
|
};
|
|
|
|
handlers.remove(idx);
|
|
self.waiter.notify_all();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod fuse_test {
|
|
use super::*;
|
|
|
|
// todo: more tests needed!
|
|
fn desc_gen(name: &str, kind: FileType) -> FileDescription {
|
|
FileDescription {
|
|
conn_id: 0,
|
|
name: PathBuf::from(name),
|
|
kind,
|
|
atime: SystemTime::UNIX_EPOCH,
|
|
last_modified: SystemTime::UNIX_EPOCH,
|
|
last_metadata_changed: SystemTime::UNIX_EPOCH,
|
|
creation_time: SystemTime::UNIX_EPOCH,
|
|
|
|
size: 0,
|
|
perm: 0,
|
|
}
|
|
}
|
|
fn generate_descriptions(prefix: &str) -> Vec<FileDescription> {
|
|
let (d0_path, f0_path, f1_path, d1_path, f2_path, f3_path) = if prefix.is_empty() {
|
|
(
|
|
"folder0".to_string(),
|
|
"folder0/file0".to_string(),
|
|
"folder0/file1".to_string(),
|
|
"folder1".to_string(),
|
|
"folder1/file2".to_string(),
|
|
"folder1/📄3".to_string(),
|
|
)
|
|
} else {
|
|
(
|
|
format!("{}/folder0", prefix),
|
|
format!("{}/folder0/file0", prefix),
|
|
format!("{}/folder0/file1", prefix),
|
|
format!("{}/folder1", prefix),
|
|
format!("{}/folder1/file2", prefix),
|
|
format!("{}/folder1/📄3", prefix),
|
|
)
|
|
};
|
|
let folder0 = desc_gen(&d0_path, FileType::Directory);
|
|
let file0 = desc_gen(&f0_path, FileType::File);
|
|
let file1 = desc_gen(&f1_path, FileType::File);
|
|
let folder1 = desc_gen(&d1_path, FileType::Directory);
|
|
let file2 = desc_gen(&f2_path, FileType::File);
|
|
let file3 = desc_gen(&f3_path, FileType::File);
|
|
|
|
vec![folder0, file0, file1, folder1, file2, file3]
|
|
}
|
|
|
|
fn build_tree(prefix: &str) {
|
|
let source_list = generate_descriptions(prefix);
|
|
|
|
let build_res = FuseNode::build_tree(source_list);
|
|
assert!(build_res.is_ok());
|
|
let tree_list = build_res.unwrap();
|
|
|
|
assert_eq!(tree_list.len(), 7);
|
|
|
|
assert_eq!(tree_list[0].name, "/");
|
|
assert_eq!(tree_list[1].name, "folder0");
|
|
assert_eq!(tree_list[2].name, "file0");
|
|
assert_eq!(tree_list[3].name, "file1");
|
|
assert_eq!(tree_list[4].name, "folder1");
|
|
assert_eq!(tree_list[5].name, "file2");
|
|
assert_eq!(tree_list[6].name, "📄3");
|
|
|
|
assert_eq!(tree_list[0].children, vec![2, 5]);
|
|
assert_eq!(tree_list[1].children, vec![3, 4]);
|
|
assert!(tree_list[2].children.is_empty());
|
|
assert!(tree_list[3].children.is_empty());
|
|
assert_eq!(tree_list[4].children, vec![6, 7]);
|
|
assert!(tree_list[5].children.is_empty());
|
|
assert!(tree_list[6].children.is_empty());
|
|
|
|
for (idx, node) in tree_list.iter().skip(1).enumerate() {
|
|
assert_eq!(idx, node.index)
|
|
}
|
|
}
|
|
|
|
fn build_single_file(prefix: &str) {
|
|
let raw_name = "simple_test_file.txt";
|
|
let f_name = if prefix == "" {
|
|
raw_name.to_string()
|
|
} else {
|
|
prefix.to_string() + "/" + raw_name
|
|
};
|
|
let desc = desc_gen(&f_name, FileType::File);
|
|
let tree = FuseNode::build_tree(vec![desc]).unwrap();
|
|
|
|
assert_eq!(tree.len(), 2);
|
|
assert_eq!(tree[0].name, "/");
|
|
assert_eq!(tree[0].children, vec![2]);
|
|
|
|
assert_eq!(tree[1].name, raw_name);
|
|
assert_eq!(tree[1].index, 0);
|
|
assert_eq!(tree[1].attributes.kind, FileType::File);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_single() {
|
|
build_single_file("");
|
|
build_single_file("/");
|
|
build_single_file("test");
|
|
build_single_file("/test");
|
|
build_single_file("🗂");
|
|
build_single_file("/🗂");
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_tree() {
|
|
build_tree("");
|
|
build_tree("/");
|
|
build_tree("test");
|
|
build_tree("/test");
|
|
build_tree("/test/test");
|
|
build_tree("🗂");
|
|
build_tree("/🗂");
|
|
build_tree("🗂/test");
|
|
}
|
|
}
|