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,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()
}
}