use fuser::{FileAttr, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, Request};
use libc::ENOENT;
use log::{debug, error, warn};
use ssh2::{ErrorCode, OpenFlags, OpenType, Session, Sftp};
use std::{
collections::HashMap,
ffi::OsStr,
io::{Read, Seek, Write},
path::{Path, PathBuf},
time::{Duration, SystemTime, UNIX_EPOCH},
};
pub struct Sshfs {
_session: Session,
sftp: Sftp,
inodes: Inodes,
fhandls: Fhandles,
_top_path: PathBuf,
}
impl Sshfs {
pub fn new(session: Session, path: &Path) -> Self {
let mut inodes = Inodes::new();
let top_path: PathBuf = path.into();
inodes.add(&top_path);
let sftp = session.sftp().unwrap();
debug!(
"[Sshfs::new] connect path: <{:?}>, inodes=<{:?}>",
&top_path, &inodes.list
);
Self {
_session: session,
sftp,
inodes,
fhandls: Fhandles::new(),
_top_path: top_path,
}
}
fn getattr_from_ssh2(&mut self, path: &Path, uid: u32, gid: u32) -> Result<FileAttr, Error> {
let attr_ssh2 = self.sftp.lstat(path)?;
let kind = Self::conv_file_kind_ssh2fuser(&attr_ssh2.file_type())?;
let ino = self.inodes.add(path);
Ok(FileAttr {
ino,
size: attr_ssh2.size.unwrap_or(0),
blocks: attr_ssh2.size.unwrap_or(0) / 512 + 1,
atime: UNIX_EPOCH + Duration::from_secs(attr_ssh2.atime.unwrap_or(0)),
mtime: UNIX_EPOCH + Duration::from_secs(attr_ssh2.mtime.unwrap_or(0)),
ctime: UNIX_EPOCH + Duration::from_secs(attr_ssh2.mtime.unwrap_or(0)),
crtime: UNIX_EPOCH,
kind,
perm: attr_ssh2.perm.unwrap_or(0o666) as u16,
nlink: 1,
uid,
gid,
rdev: 0,
blksize: 512,
flags: 0,
})
}
fn conv_file_kind_ssh2fuser(filetype: &ssh2::FileType) -> Result<fuser::FileType, Error> {
match filetype {
ssh2::FileType::NamedPipe => Ok(fuser::FileType::NamedPipe),
ssh2::FileType::CharDevice => Ok(fuser::FileType::CharDevice),
ssh2::FileType::BlockDevice => Ok(fuser::FileType::BlockDevice),
ssh2::FileType::Directory => Ok(fuser::FileType::Directory),
ssh2::FileType::RegularFile => Ok(fuser::FileType::RegularFile),
ssh2::FileType::Symlink => Ok(fuser::FileType::Symlink),
ssh2::FileType::Socket => Ok(fuser::FileType::Socket),
ssh2::FileType::Other(_) => Err(Error(libc::EBADF)),
}
}
fn conv_timeornow2systemtime(time: &fuser::TimeOrNow) -> SystemTime {
match time {
fuser::TimeOrNow::SpecificTime(t) => *t,
fuser::TimeOrNow::Now => SystemTime::now(),
}
}
}
impl Filesystem for Sshfs {
fn lookup(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
let Some(mut path) = self.inodes.get_path(parent) else {
debug!("[lookup] 親ディレクトリの検索に失敗 inode={}", parent);
reply.error(ENOENT);
return;
};
path.push(Path::new(name));
match self.getattr_from_ssh2(&path, req.uid(), req.gid()) {
Ok(attr) => reply.entry(&Duration::from_secs(1), &attr, 0),
Err(e) => {
reply.error(e.0);
}
};
}
fn getattr(&mut self, req: &Request, ino: u64, reply: ReplyAttr) {
let Some(path) = self.inodes.get_path(ino) else {
debug!("[getattr] path取得失敗: inode={}", ino);
reply.error(ENOENT);
return;
};
match self.getattr_from_ssh2(&path, req.uid(), req.gid()) {
Ok(attr) => {
reply.attr(&Duration::from_secs(1), &attr);
}
Err(e) => {
warn!("[getattr] getattr_from_ssh2エラー: {:?}", &e);
reply.error(e.0)
}
};
}
fn readdir(
&mut self,
_req: &Request,
ino: u64,
_fh: u64,
offset: i64,
mut reply: ReplyDirectory,
) {
let Some(path) = self.inodes.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
match self.sftp.readdir(&path) {
Ok(mut dir) => {
let cur_file_attr = ssh2::FileStat {
size: None,
uid: None,
gid: None,
perm: Some(libc::S_IFDIR),
atime: None,
mtime: None,
}; dir.insert(0, (Path::new("..").into(), cur_file_attr.clone()));
dir.insert(0, (Path::new(".").into(), cur_file_attr));
let mut i = offset + 1;
for f in dir.iter().skip(offset as usize) {
let ino = if f.0 == Path::new("..") || f.0 == Path::new(".") {
1
} else {
self.inodes.add(&f.0)
};
let name = match f.0.file_name() {
Some(n) => n,
None => f.0.as_os_str(),
};
let filetype = &f.1.file_type();
let filetype = match Self::conv_file_kind_ssh2fuser(filetype) {
Ok(t) => t,
Err(e) => {
warn!(
"[readdir]ファイルタイプ解析失敗: inode={}, name={:?}",
ino, name
);
reply.error(e.0);
return;
}
};
if reply.add(ino, i, filetype, name) {
break;
}
i += 1;
}
reply.ok();
}
Err(e) => {
warn!("[readdir]ssh2::readdir内でエラー発生-- {:?}", e);
reply.error(Error::from(e).0);
}
};
}
fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) {
let Some(path) = self.inodes.get_path(ino) else {
error!("[readlink] 親ディレクトリの検索に失敗 {ino}");
reply.error(libc::ENOENT);
return;
};
match self.sftp.readlink(&path) {
Ok(p) => {
reply.data(p.as_os_str().to_str().unwrap().as_bytes());
}
Err(e) => {
reply.error(Error::from(e).0);
}
}
}
fn open(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
let Some(file_name) = self.inodes.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let mut flags_ssh2 = OpenFlags::empty();
if flags & libc::O_WRONLY != 0 {
flags_ssh2.insert(OpenFlags::WRITE);
} else if flags & libc::O_RDWR != 0 {
flags_ssh2.insert(OpenFlags::READ);
flags_ssh2.insert(OpenFlags::WRITE);
} else {
flags_ssh2.insert(OpenFlags::READ);
}
if flags & libc::O_APPEND != 0 {
flags_ssh2.insert(OpenFlags::APPEND);
}
if flags & libc::O_CREAT != 0 {
flags_ssh2.insert(OpenFlags::CREATE);
}
if flags & libc::O_TRUNC != 0 {
flags_ssh2.insert(OpenFlags::TRUNCATE);
}
if flags & libc::O_EXCL != 0 {
flags_ssh2.insert(OpenFlags::EXCLUSIVE);
}
debug!(
"[open] openflag = {:?}, bit = {:x}",
&flags_ssh2,
flags_ssh2.bits()
);
match self
.sftp
.open_mode(&file_name, flags_ssh2, 0o777, ssh2::OpenType::File)
{
Ok(file) => {
let fh = self.fhandls.add_file(file);
reply.opened(fh, flags as u32);
}
Err(e) => reply.error(Error::from(e).0),
}
}
fn release(
&mut self,
_req: &Request<'_>,
_ino: u64,
fh: u64,
_flags: i32,
_lock_owner: Option<u64>,
_flush: bool,
reply: fuser::ReplyEmpty,
) {
self.fhandls.del_file(fh);
reply.ok();
}
fn read(
&mut self,
_req: &Request,
_ino: u64,
fh: u64,
offset: i64,
size: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: ReplyData,
) {
let Some(file) = self.fhandls.get_file(fh) else {
reply.error(libc::EINVAL);
return;
};
if let Err(e) = file.seek(std::io::SeekFrom::Start(offset as u64)) {
reply.error(Error::from(e).0);
return;
}
let mut buff = vec![0; size as usize];
let mut read_size: usize = 0;
while read_size < size as usize {
match file.read(&mut buff[read_size..]) {
Ok(s) => {
if s == 0 {
break;
};
read_size += s;
}
Err(e) => {
reply.error(Error::from(e).0);
return;
}
}
}
buff.resize(read_size, 0u8);
reply.data(&buff);
}
fn write(
&mut self,
_req: &Request<'_>,
_ino: u64,
fh: u64,
offset: i64,
data: &[u8],
_write_flags: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: fuser::ReplyWrite,
) {
let Some(file) = self.fhandls.get_file(fh) else {
reply.error(libc::EINVAL);
return;
};
if let Err(e) = file.seek(std::io::SeekFrom::Start(offset as u64)) {
reply.error(Error::from(e).0);
return;
}
let mut buf = data;
while !buf.is_empty() {
let cnt = match file.write(buf) {
Ok(cnt) => cnt,
Err(e) => {
reply.error(Error::from(e).0);
return;
}
};
buf = &buf[cnt..];
}
reply.written(data.len() as u32);
}
fn mknod(
&mut self,
req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
umask: u32,
_rdev: u32,
reply: ReplyEntry,
) {
if mode & libc::S_IFMT != libc::S_IFREG {
reply.error(libc::EPERM);
return;
}
let mode = mode & (!umask | libc::S_IFMT);
let Some(mut new_name) = self.inodes.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
new_name.push(name);
if let Err(e) =
self.sftp
.open_mode(&new_name, OpenFlags::CREATE, mode as i32, OpenType::File)
{
reply.error(Error::from(e).0);
return;
}
let new_attr = match self.getattr_from_ssh2(&new_name, req.uid(), req.gid()) {
Ok(a) => a,
Err(e) => {
reply.error(e.0);
return;
}
};
reply.entry(&Duration::from_secs(1), &new_attr, 0);
}
fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: fuser::ReplyEmpty) {
let Some(mut path) = self.inodes.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
path.push(name);
match self.sftp.unlink(&path) {
Ok(_) => {
self.inodes.del_inode_with_path(&path);
reply.ok();
}
Err(e) => reply.error(Error::from(e).0),
}
}
fn mkdir(
&mut self,
req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
umask: u32,
reply: ReplyEntry,
) {
let Some(mut path) = self.inodes.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
path.push(name);
let mode = (mode & (!umask) & 0o777) as i32;
match self.sftp.mkdir(&path, mode) {
Ok(_) => match self.getattr_from_ssh2(&path, req.uid(), req.gid()) {
Ok(attr) => reply.entry(&Duration::from_secs(1), &attr, 0),
Err(e) => reply.error(e.0),
},
Err(e) => reply.error(Error::from(e).0),
}
}
fn rmdir(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: fuser::ReplyEmpty) {
let Some(mut path) = self.inodes.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
path.push(name);
match self.sftp.rmdir(&path) {
Ok(_) => {
self.inodes.del_inode_with_path(&path);
reply.ok()
}
Err(e) => {
if e.code() == ErrorCode::Session(-31) {
reply.error(libc::ENOTEMPTY);
} else {
reply.error(Error::from(e).0)
}
}
}
}
fn symlink(
&mut self,
req: &Request<'_>,
parent: u64,
name: &OsStr,
link: &Path,
reply: ReplyEntry,
) {
let Some(mut target) = self.inodes.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
target.push(name);
match self.sftp.symlink(link, &target) {
Ok(_) => match self.getattr_from_ssh2(&target, req.uid(), req.gid()) {
Ok(attr) => reply.entry(&Duration::from_secs(1), &attr, 0),
Err(e) => reply.error(e.0),
},
Err(e) => reply.error(Error::from(e).0),
}
}
fn setattr(
&mut self,
req: &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<std::time::SystemTime>,
_fh: Option<u64>,
_crtime: Option<std::time::SystemTime>,
_chgtime: Option<std::time::SystemTime>,
_bkuptime: Option<std::time::SystemTime>,
_flags: Option<u32>,
reply: ReplyAttr,
) {
let stat = ssh2::FileStat {
size,
uid: None,
gid: None,
perm: mode,
atime: atime.map(|t| {
Self::conv_timeornow2systemtime(&t)
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}),
mtime: mtime.map(|t| {
Self::conv_timeornow2systemtime(&t)
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}),
};
let Some(filename) = self.inodes.get_path(ino) else {
reply.error(ENOENT);
return;
};
match self.sftp.setstat(&filename, stat) {
Ok(_) => {
let stat = self.getattr_from_ssh2(&filename, req.uid(), req.gid());
match stat {
Ok(s) => reply.attr(&Duration::from_secs(1), &s),
Err(e) => reply.error(e.0),
}
}
Err(e) => reply.error(Error::from(e).0),
}
}
fn rename(
&mut self,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
newparent: u64,
newname: &OsStr,
flags: u32,
reply: fuser::ReplyEmpty,
) {
let Some(mut old_path) = self.inodes.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
old_path.push(name);
let Some(mut new_path) = self.inodes.get_path(newparent) else {
reply.error(libc::ENOENT);
return;
};
new_path.push(newname);
let mut rename_flag = ssh2::RenameFlags::NATIVE;
if flags & libc::RENAME_EXCHANGE != 0 {
rename_flag.insert(ssh2::RenameFlags::ATOMIC);
}
if flags & libc::RENAME_NOREPLACE == 0 {
if let Ok(stat) = self.sftp.lstat(&new_path) {
if stat.is_dir() {
if let Err(e) = self.sftp.rmdir(&new_path) {
reply.error(Error::from(e).0);
return;
}
} else if let Err(e) = self.sftp.unlink(&new_path) {
reply.error(Error::from(e).0);
return;
}
self.inodes.del_inode_with_path(&new_path);
}
}
match self.sftp.rename(&old_path, &new_path, Some(rename_flag)) {
Ok(_) => {
self.inodes.rename(&old_path, &new_path);
reply.ok();
}
Err(e) => reply.error(Error::from(e).0),
}
}
}
#[derive(Debug, Default)]
struct Inodes {
list: HashMap<u64, PathBuf>,
max_inode: u64,
}
impl Inodes {
fn new() -> Self {
Self {
list: std::collections::HashMap::new(),
max_inode: 0,
}
}
fn add(&mut self, path: &Path) -> u64 {
match self.get_inode(path) {
Some(i) => i,
None => {
self.max_inode += 1;
self.list.insert(self.max_inode, path.into());
self.max_inode
}
}
}
fn get_inode(&self, path: &Path) -> Option<u64> {
self.list.iter().find(|(_, p)| path == *p).map(|(i, _)| *i)
}
fn get_path(&self, inode: u64) -> Option<PathBuf> {
self.list.get(&inode).map(|p| (*p).clone())
}
fn del_inode(&mut self, inode: u64) -> Option<u64> {
self.list.remove(&inode).map(|_| inode)
}
fn del_inode_with_path(&mut self, path: &Path) -> Option<u64> {
self.get_inode(path).map(|ino| self.del_inode(ino).unwrap())
}
fn rename(&mut self, old_path: &Path, new_path: &Path) {
let Some(ino) = self.get_inode(old_path) else {
return;
};
if let Some(val) = self.list.get_mut(&ino) {
*val = new_path.into();
}
}
}
struct Fhandles {
list: HashMap<u64, ssh2::File>,
next_handle: u64,
}
impl Fhandles {
fn new() -> Self {
Self {
list: HashMap::new(),
next_handle: 0,
}
}
fn add_file(&mut self, file: ssh2::File) -> u64 {
let handle = self.next_handle;
self.list.insert(handle, file);
self.next_handle += 1;
handle
}
fn get_file(&mut self, fh: u64) -> Option<&mut ssh2::File> {
self.list.get_mut(&fh)
}
fn del_file(&mut self, fh: u64) {
self.list.remove(&fh); match self.list.keys().max() {
Some(&i) => self.next_handle = i + 1,
None => self.next_handle = 0,
}
}
}
#[derive(Debug, Clone, Copy)]
struct Error(i32);
impl From<ssh2::Error> for Error {
fn from(value: ssh2::Error) -> Self {
let eno = match value.code() {
ssh2::ErrorCode::Session(_) => libc::ENXIO,
ssh2::ErrorCode::SFTP(i) => match i {
2 => libc::ENOENT, 3 => libc::EACCES, 4 => libc::EIO, 5 => libc::ENODEV, 6 => libc::ENXIO, 7 => libc::ENETDOWN, 8 => libc::ENODEV, 9 => libc::EBADF, 10 => libc::ENOENT, 11 => libc::EEXIST, 12 => libc::EACCES, 13 => libc::ENXIO, 14 => libc::ENOSPC, 15 => libc::EDQUOT, 16 => libc::ENODEV, 17 => libc::ENOLCK, 18 => libc::ENOTEMPTY, 19 => libc::ENOTDIR, 20 => libc::ENAMETOOLONG, 21 => libc::ELOOP, _ => 0,
},
};
Self(eno)
}
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
use std::io::ErrorKind::*;
let eno = match value.kind() {
NotFound => libc::ENOENT,
PermissionDenied => libc::EACCES,
ConnectionRefused => libc::ECONNREFUSED,
ConnectionReset => libc::ECONNRESET,
ConnectionAborted => libc::ECONNABORTED,
NotConnected => libc::ENOTCONN,
AddrInUse => libc::EADDRINUSE,
AddrNotAvailable => libc::EADDRNOTAVAIL,
BrokenPipe => libc::EPIPE,
AlreadyExists => libc::EEXIST,
WouldBlock => libc::EWOULDBLOCK,
InvalidInput => libc::EINVAL,
InvalidData => libc::EILSEQ,
TimedOut => libc::ETIMEDOUT,
WriteZero => libc::EIO,
Interrupted => libc::EINTR,
Unsupported => libc::ENOTSUP,
UnexpectedEof => libc::EOF,
OutOfMemory => libc::ENOMEM,
_ => 0,
};
Self(eno)
}
}
#[cfg(test)]
mod inode_test {
use super::Inodes;
use std::path::Path;
#[test]
fn inode_add_test() {
let mut inodes = Inodes::new();
assert_eq!(inodes.add(Path::new("")), 1);
assert_eq!(inodes.add(Path::new("test")), 2);
assert_eq!(inodes.add(Path::new("")), 1);
assert_eq!(inodes.add(Path::new("test")), 2);
assert_eq!(inodes.add(Path::new("test3")), 3);
assert_eq!(inodes.add(Path::new("/test")), 4);
assert_eq!(inodes.add(Path::new("test/")), 2);
}
fn make_inodes() -> Inodes {
let mut inodes = Inodes::new();
inodes.add(Path::new(""));
inodes.add(Path::new("test"));
inodes.add(Path::new("test2"));
inodes.add(Path::new("test3/"));
inodes
}
#[test]
fn inodes_get_inode_test() {
let inodes = make_inodes();
assert_eq!(inodes.get_inode(Path::new("")), Some(1));
assert_eq!(inodes.get_inode(Path::new("test4")), None);
assert_eq!(inodes.get_inode(Path::new("/test")), None);
assert_eq!(inodes.get_inode(Path::new("test3")), Some(4));
}
#[test]
fn inodes_get_path_test() {
let inodes = make_inodes();
assert_eq!(inodes.get_path(1), Some(Path::new("").into()));
assert_eq!(inodes.get_path(3), Some(Path::new("test2").into()));
assert_eq!(inodes.get_path(5), None);
assert_eq!(inodes.get_path(3), Some(Path::new("test2/").into()));
}
#[test]
fn inodes_rename() {
let mut inodes = make_inodes();
let old = Path::new("test2");
let new = Path::new("new_test");
let ino = inodes.get_inode(old).unwrap();
inodes.rename(old, new);
assert_eq!(inodes.get_path(ino), Some(new.into()));
let mut inodes = make_inodes();
let inodes2 = make_inodes();
inodes.rename(Path::new("nai"), Path::new("kawattenai"));
assert_eq!(inodes.list, inodes2.list);
}
}