VFS/DataProvider/Config refactoring + SSH public key authentication
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

Phase 1-6 of refactoring plan:
- VFS abstraction (VfsBackend trait + LocalFs + OpenFlags builder)
- DataProvider trait (SqliteProvider + PgProvider, SFTPGo-compatible)
- Config refactoring (AppConfig unified sections, env overrides)
- SSH handlers (sftp/scp/rsync) migrated to VFS + DataProvider
- SSH public key authentication (Ed25519 signature verification)
- SSH stderr → CHANNEL_EXTENDED_DATA support
- Web auth uses DataProvider instead of direct SQL
- User home directory from provider (per-user isolation)
- PostgreSQL auth provider for SFTPGo compatibility
This commit is contained in:
Warren
2026-06-18 23:35:18 +08:00
parent 83fb0de78a
commit f90e4f496c
25 changed files with 2039 additions and 612 deletions

View File

@@ -0,0 +1,212 @@
use super::util;
use super::open_flags::OpenFlags;
use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsStat};
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
/// 本地文件系统实现(直接包装 std::fs不做路径解析
/// 路径解析由上层SftpHandler负责
pub struct LocalFs;
impl LocalFs {
pub fn new() -> Self {
Self
}
}
struct LocalFile {
file: File,
}
impl VfsFile for LocalFile {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, VfsError> {
self.file.read(buf).map_err(|e| VfsError::Io(e.to_string()))
}
fn write(&mut self, buf: &[u8]) -> Result<usize, VfsError> {
self.file.write(buf).map_err(|e| VfsError::Io(e.to_string()))
}
fn seek(&mut self, pos: SeekFrom) -> Result<u64, VfsError> {
self.file.seek(pos).map_err(|e| VfsError::Io(e.to_string()))
}
fn flush(&mut self) -> Result<(), VfsError> {
self.file.flush().map_err(|e| VfsError::Io(e.to_string()))
}
fn stat(&mut self) -> Result<VfsStat, VfsError> {
let meta = self.file.metadata().map_err(|e| VfsError::Io(e.to_string()))?;
Ok(util::stat_from_metadata(&meta, false))
}
fn set_len(&mut self, size: u64) -> Result<(), VfsError> {
self.file.set_len(size).map_err(|e| VfsError::Io(e.to_string()))
}
}
impl VfsBackend for LocalFs {
fn read_dir(&self, path: &Path) -> Result<Vec<VfsDirEntry>, VfsError> {
let dir = fs::read_dir(path).map_err(|e| util::map_io_error(path, e))?;
let mut entries = Vec::new();
for entry in dir {
let entry = entry.map_err(|e| util::map_io_error(path, e))?;
let name = entry.file_name().to_string_lossy().to_string();
let file_type = entry.file_type().map_err(|e| util::map_io_error(path, e))?;
let meta = entry.metadata().map_err(|e| util::map_io_error(path, e))?;
let stat = util::stat_from_metadata(&meta, file_type.is_symlink());
let long_name = util::build_long_name(&stat, &name);
entries.push(VfsDirEntry {
name,
long_name,
stat,
});
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
}
fn open_file(&self, path: &Path, flags: &OpenFlags) -> Result<Box<dyn VfsFile>, VfsError> {
let mut opts = OpenOptions::new();
opts.read(flags.read);
opts.write(flags.write);
opts.append(flags.append);
opts.create(flags.create);
opts.truncate(flags.truncate);
opts.create_new(flags.exclusive);
let file = opts.open(path).map_err(|e| util::map_io_error(path, e))?;
#[cfg(unix)]
if flags.create && !flags.exclusive {
if let Ok(meta) = file.metadata() {
if flags.mode != 0 && meta.permissions().mode() != flags.mode {
fs::set_permissions(path, std::fs::Permissions::from_mode(flags.mode))
.ok();
}
}
}
Ok(Box::new(LocalFile { file }))
}
fn stat(&self, path: &Path) -> Result<VfsStat, VfsError> {
let meta = fs::metadata(path).map_err(|e| util::map_io_error(path, e))?;
Ok(util::stat_from_metadata(&meta, false))
}
fn lstat(&self, path: &Path) -> Result<VfsStat, VfsError> {
let meta = fs::symlink_metadata(path).map_err(|e| util::map_io_error(path, e))?;
let is_symlink = path.is_symlink() || meta.file_type().is_symlink();
Ok(util::stat_from_metadata(&meta, is_symlink))
}
fn create_dir(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
fs::create_dir(path).map_err(|e| util::map_io_error(path, e))?;
#[cfg(unix)]
{
fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
.map_err(|e| util::map_io_error(path, e))?;
}
Ok(())
}
fn create_dir_all(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
fs::create_dir_all(path).map_err(|e| util::map_io_error(path, e))?;
#[cfg(unix)]
{
if mode != 0 {
fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
.map_err(|e| util::map_io_error(path, e))?;
}
}
Ok(())
}
fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
fs::remove_dir(path).map_err(|e| util::map_io_error(path, e))
}
fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
fs::remove_file(path).map_err(|e| util::map_io_error(path, e))
}
fn rename(&self, from: &Path, to: &Path) -> Result<(), VfsError> {
fs::rename(from, to).map_err(|e| util::map_io_error(from, e))
}
fn set_stat(&self, path: &Path, stat: &VfsStat) -> Result<(), VfsError> {
#[cfg(unix)]
{
if stat.mode != 0 {
fs::set_permissions(path, std::fs::Permissions::from_mode(stat.mode))
.map_err(|e| util::map_io_error(path, e))?;
}
}
if let (Some(atime), Some(mtime)) = (
stat.atime.duration_since(std::time::UNIX_EPOCH).ok(),
stat.mtime.duration_since(std::time::UNIX_EPOCH).ok(),
) {
filetime::set_file_times(path,
filetime::FileTime::from_unix_time(atime.as_secs() as i64, 0),
filetime::FileTime::from_unix_time(mtime.as_secs() as i64, 0),
).map_err(|e| util::map_io_error(path, e))?;
}
Ok(())
}
fn read_link(&self, path: &Path) -> Result<PathBuf, VfsError> {
let target = fs::read_link(path).map_err(|e| util::map_io_error(path, e))?;
Ok(target)
}
fn create_symlink(&self, target: &Path, link: &Path) -> Result<(), VfsError> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, link)
.map_err(|e| util::map_io_error(link, e))?;
}
#[cfg(not(unix))]
{
std::os::windows::fs::symlink_file(target, link)
.map_err(|e| util::map_io_error(link, e))?;
}
Ok(())
}
fn real_path(&self, path: &Path) -> Result<PathBuf, VfsError> {
let canonical = path.canonicalize().map_err(|e| util::map_io_error(path, e))?;
Ok(canonical)
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn hard_link(&self, original: &Path, link: &Path) -> Result<(), VfsError> {
#[cfg(unix)]
{
fs::hard_link(original, link).map_err(|e| util::map_io_error(original, e))?;
}
#[cfg(not(unix))]
{
return Err(VfsError::Unsupported("hard_link not supported on non-Unix systems".to_string()));
}
Ok(())
}
}

View File

@@ -0,0 +1,160 @@
pub mod open_flags;
pub mod local_fs;
pub mod util;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
/// VFS 错误类型
#[derive(Debug, Clone)]
pub enum VfsError {
NotFound(String),
PermissionDenied(String),
AlreadyExists(String),
NotEmpty(String),
NotADirectory(String),
IsADirectory(String),
Unsupported(String),
Io(String),
UnexpectedEof,
}
impl std::fmt::Display for VfsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VfsError::NotFound(p) => write!(f, "No such file or directory: {}", p),
VfsError::PermissionDenied(p) => write!(f, "Permission denied: {}", p),
VfsError::AlreadyExists(p) => write!(f, "File already exists: {}", p),
VfsError::NotEmpty(p) => write!(f, "Directory not empty: {}", p),
VfsError::NotADirectory(p) => write!(f, "Not a directory: {}", p),
VfsError::IsADirectory(p) => write!(f, "Is a directory: {}", p),
VfsError::Unsupported(msg) => write!(f, "Unsupported: {}", msg),
VfsError::Io(msg) => write!(f, "IO error: {}", msg),
VfsError::UnexpectedEof => write!(f, "Unexpected end of file"),
}
}
}
impl std::error::Error for VfsError {}
/// 文件统计信息(类似 libc::stat
#[derive(Debug, Clone)]
pub struct VfsStat {
pub size: u64,
pub mode: u32,
pub uid: u32,
pub gid: u32,
pub atime: SystemTime,
pub mtime: SystemTime,
pub is_dir: bool,
pub is_symlink: bool,
}
impl VfsStat {
pub fn new() -> Self {
Self {
size: 0,
mode: 0,
uid: 0,
gid: 0,
atime: SystemTime::UNIX_EPOCH,
mtime: SystemTime::UNIX_EPOCH,
is_dir: false,
is_symlink: false,
}
}
}
impl Default for VfsStat {
fn default() -> Self {
Self::new()
}
}
/// 目录条目
#[derive(Debug, Clone)]
pub struct VfsDirEntry {
pub name: String,
pub long_name: String,
pub stat: VfsStat,
}
/// 打开文件的抽象
pub trait VfsFile {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, VfsError>;
fn write(&mut self, buf: &[u8]) -> Result<usize, VfsError>;
fn seek(&mut self, pos: std::io::SeekFrom) -> Result<u64, VfsError>;
fn flush(&mut self) -> Result<(), VfsError>;
fn stat(&mut self) -> Result<VfsStat, VfsError>;
fn set_len(&mut self, size: u64) -> Result<(), VfsError>;
/// Write all bytes (convenience, default loops write() until done)
fn write_all(&mut self, mut buf: &[u8]) -> Result<(), VfsError> {
while !buf.is_empty() {
let n = self.write(buf)?;
if n == 0 {
return Err(VfsError::Io("write returned 0".to_string()));
}
buf = &buf[n..];
}
Ok(())
}
/// Read exactly `buf.len()` bytes (convenience, loops read() until done)
fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<(), VfsError> {
while !buf.is_empty() {
let n = self.read(buf)?;
if n == 0 {
return Err(VfsError::UnexpectedEof);
}
buf = &mut buf[n..];
}
Ok(())
}
}
/// VFS 后端 trait所有文件系统操作
pub trait VfsBackend: Send {
/// 读取目录内容
fn read_dir(&self, path: &Path) -> Result<Vec<VfsDirEntry>, VfsError>;
/// 打开文件(读/写)
fn open_file(&self, path: &Path, flags: &open_flags::OpenFlags) -> Result<Box<dyn VfsFile>, VfsError>;
/// 获取文件/目录元数据
fn stat(&self, path: &Path) -> Result<VfsStat, VfsError>;
fn lstat(&self, path: &Path) -> Result<VfsStat, VfsError>;
/// 创建目录
fn create_dir(&self, path: &Path, mode: u32) -> Result<(), VfsError>;
/// 递归创建目录
fn create_dir_all(&self, path: &Path, mode: u32) -> Result<(), VfsError>;
/// 删除空目录
fn remove_dir(&self, path: &Path) -> Result<(), VfsError>;
/// 删除文件
fn remove_file(&self, path: &Path) -> Result<(), VfsError>;
/// 重命名
fn rename(&self, from: &Path, to: &Path) -> Result<(), VfsError>;
/// 设置文件属性
fn set_stat(&self, path: &Path, stat: &VfsStat) -> Result<(), VfsError>;
/// 读取符号链接目标
fn read_link(&self, path: &Path) -> Result<PathBuf, VfsError>;
/// 创建符号链接
fn create_symlink(&self, target: &Path, link: &Path) -> Result<(), VfsError>;
/// 规范化路径
fn real_path(&self, path: &Path) -> Result<PathBuf, VfsError>;
/// 检查路径是否存在
fn exists(&self, path: &Path) -> bool;
/// 创建硬链接
fn hard_link(&self, original: &Path, link: &Path) -> Result<(), VfsError>;
}

View File

@@ -0,0 +1,75 @@
/// 文件打开标志(映射 SSH_FXF_* 和 POSIX open flags
#[derive(Debug, Clone, Default)]
pub struct OpenFlags {
pub read: bool,
pub write: bool,
pub append: bool,
pub create: bool,
pub truncate: bool,
pub exclusive: bool,
pub mode: u32,
}
impl OpenFlags {
pub fn new() -> Self {
Self::default()
}
pub fn read(mut self) -> Self {
self.read = true;
self
}
pub fn write(mut self) -> Self {
self.write = true;
self
}
pub fn append(mut self) -> Self {
self.append = true;
self.write = true;
self
}
pub fn create(mut self) -> Self {
self.create = true;
self.write = true;
self
}
pub fn truncate(mut self) -> Self {
self.truncate = true;
self.write = true;
self
}
pub fn exclusive(mut self) -> Self {
self.exclusive = true;
self
}
pub fn mode(mut self, mode: u32) -> Self {
self.mode = mode;
self
}
/// 从 SFTP 的 pflagsSSH_FXF_*)构建 OpenFlags
pub fn from_sftp_pflags(pflags: u32) -> Self {
let read = pflags & 0x00000001 != 0;
let write = pflags & 0x00000002 != 0;
let append = pflags & 0x00000004 != 0;
let create = pflags & 0x00000008 != 0;
let truncate = pflags & 0x00000010 != 0;
let exclusive = pflags & 0x00000020 != 0;
Self {
read,
write,
append,
create,
truncate,
exclusive,
mode: 0o644,
}
}
}

View File

@@ -0,0 +1,105 @@
use super::{VfsError, VfsStat};
use chrono::Datelike;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
/// 从 std::io::ErrorKind 映射 VfsError
pub fn map_io_error(path: &Path, e: std::io::Error) -> VfsError {
match e.kind() {
std::io::ErrorKind::NotFound => VfsError::NotFound(path.display().to_string()),
std::io::ErrorKind::PermissionDenied => VfsError::PermissionDenied(path.display().to_string()),
std::io::ErrorKind::AlreadyExists => VfsError::AlreadyExists(path.display().to_string()),
std::io::ErrorKind::DirectoryNotEmpty => VfsError::NotEmpty(path.display().to_string()),
std::io::ErrorKind::NotADirectory => VfsError::NotADirectory(path.display().to_string()),
std::io::ErrorKind::IsADirectory => VfsError::IsADirectory(path.display().to_string()),
std::io::ErrorKind::UnexpectedEof => VfsError::UnexpectedEof,
other => VfsError::Io(format!("{}: {}", other, path.display())),
}
}
/// 从 std::fs::Metadata 构建 VfsStat
pub fn stat_from_metadata(meta: &std::fs::Metadata, is_symlink: bool) -> VfsStat {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
let mut stat = VfsStat::new();
stat.size = meta.len();
stat.is_dir = meta.is_dir();
stat.is_symlink = is_symlink;
#[cfg(unix)]
{
stat.mode = meta.permissions().mode();
stat.uid = meta.uid();
stat.gid = meta.gid();
}
#[cfg(not(unix))]
{
stat.mode = if meta.is_dir() { 0o40755 } else { 0o100644 };
}
if let Ok(t) = meta.accessed() {
stat.atime = t;
}
if let Ok(t) = meta.modified() {
stat.mtime = t;
}
stat
}
/// 构建目录条目的 long_name类似 ls -l 格式)
pub fn build_long_name(stat: &VfsStat, name: &str) -> String {
let file_type = if stat.is_dir { 'd' } else { '-' };
let perms = format_permissions(stat.mode & 0o777);
let link_count = if stat.is_dir { 3 } else { 1 };
let size = stat.size;
let mtime = match stat.mtime.duration_since(std::time::UNIX_EPOCH) {
Ok(d) => {
let secs = d.as_secs();
format_timestamp(secs)
}
Err(_) => "Jan 1 1970".to_string(),
};
format!(
"{}{} {} {} {} {} {} {}",
file_type, perms,
link_count,
stat.uid,
stat.gid,
size,
mtime,
name
)
}
fn format_permissions(mode: u32) -> String {
let rwx = |n: u32| -> String {
let r = if n & 4 != 0 { 'r' } else { '-' };
let w = if n & 2 != 0 { 'w' } else { '-' };
let x = if n & 1 != 0 { 'x' } else { '-' };
format!("{}{}{}", r, w, x)
};
format!(
"{}{}{}",
rwx((mode >> 6) & 7),
rwx((mode >> 3) & 7),
rwx(mode & 7)
)
}
fn format_timestamp(secs: u64) -> String {
let datetime = match chrono::DateTime::from_timestamp(secs as i64, 0) {
Some(dt) => dt,
None => return "Jan 1 1970".to_string(),
};
let now = chrono::Utc::now();
if datetime.year() == now.year() {
datetime.format("%b %e %H:%M").to_string()
} else {
datetime.format("%b %e %Y").to_string()
}
}