SMB Server Phase 2: VFS backend build fix + integration test
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- Add VfsFile: Send supertrait for Mutex compatibility
- Fix SmbServerCommand: struct → Subcommand enum with Start variant
- Fix tracing_subscriber::init() → try_init() to avoid panic when
  logger already initialized
- Fix CLI subcommand name: smb-server → smb-start (flatten naming)
- Add #[command(name = "smb-start")] for CLI disambiguation
- Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs)
- Remove unused VfsFile imports (webdav.rs, scp_handler.rs)
- Integration test: Docker smbclient verified (list, upload, read)
This commit is contained in:
Warren
2026-06-20 19:42:29 +08:00
parent 45d050c0b3
commit 7eb528d35f
167 changed files with 59897 additions and 12 deletions

View File

@@ -0,0 +1,437 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::SystemTime;
use async_trait::async_trait;
use bytes::Bytes;
use smb_server::{
BackendCapabilities, DirEntry, FileInfo, FileTimes, Handle, OpenIntent, OpenOptions, ShareBackend,
SmbError, SmbPath,
};
use super::open_flags::OpenFlags;
use super::{VfsBackend, VfsError, VfsStat};
const FILETIME_OFFSET: u64 = 116_444_736_000_000_000;
pub struct VfsShareBackend {
vfs: Arc<dyn VfsBackend>,
root: PathBuf,
read_only: bool,
}
impl VfsShareBackend {
pub fn new(vfs: Box<dyn VfsBackend>, root: PathBuf) -> Self {
Self {
vfs: Arc::from(vfs),
root,
read_only: false,
}
}
pub fn read_only(mut self, yes: bool) -> Self {
self.read_only = yes;
self
}
}
fn resolve_path(root: &Path, smb_path: &SmbPath) -> PathBuf {
if smb_path.is_root() {
return root.to_path_buf();
}
let mut result = root.to_path_buf();
for component in smb_path.components() {
result.push(component);
}
result
}
fn map_error(e: VfsError) -> SmbError {
match e {
VfsError::NotFound(_) => SmbError::NotFound,
VfsError::PermissionDenied(_) => SmbError::AccessDenied,
VfsError::AlreadyExists(_) => SmbError::Exists,
VfsError::NotEmpty(_) => SmbError::NotEmpty,
VfsError::NotADirectory(_) => SmbError::NotADirectory,
VfsError::IsADirectory(_) => SmbError::IsDirectory,
VfsError::Unsupported(_) => SmbError::NotSupported,
VfsError::Io(msg) => SmbError::Io(std::io::Error::other(msg)),
VfsError::UnexpectedEof => SmbError::Io(std::io::Error::other("unexpected eof")),
}
}
fn system_time_to_filetime(t: SystemTime) -> u64 {
match t.duration_since(SystemTime::UNIX_EPOCH) {
Ok(d) => {
FILETIME_OFFSET
+ (d.as_secs() * 10_000_000)
+ (d.subsec_nanos() as u64 / 100)
}
Err(_) => 0,
}
}
fn vfs_stat_to_file_info(stat: &VfsStat, name: &str, path: &Path) -> FileInfo {
let name = if name.is_empty() {
path.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string()
} else {
name.to_string()
};
FileInfo {
name,
end_of_file: stat.size,
allocation_size: stat.size,
creation_time: system_time_to_filetime(stat.mtime),
last_access_time: system_time_to_filetime(stat.atime),
last_write_time: system_time_to_filetime(stat.mtime),
change_time: system_time_to_filetime(stat.mtime),
is_directory: stat.is_dir,
file_index: 0,
}
}
fn vfs_error_to_io(e: VfsError) -> std::io::Error {
std::io::Error::other(e.to_string())
}
#[async_trait]
impl ShareBackend for VfsShareBackend {
async fn open(&self, path: &SmbPath, opts: OpenOptions) -> Result<Box<dyn Handle>, SmbError> {
let full_path = resolve_path(&self.root, path);
if opts.directory {
match opts.intent {
OpenIntent::Create => {
if self.vfs.exists(&full_path) {
return Err(SmbError::Exists);
}
self.vfs.create_dir(&full_path, 0o755).map_err(map_error)?;
}
OpenIntent::OpenOrCreate | OpenIntent::OverwriteOrCreate => {
if !self.vfs.exists(&full_path) {
self.vfs.create_dir(&full_path, 0o755).map_err(map_error)?;
}
}
_ => {
if !self.vfs.exists(&full_path) {
return Err(SmbError::NotFound);
}
}
}
let stat = self.vfs.stat(&full_path).map_err(map_error)?;
if !stat.is_dir {
return Err(SmbError::NotADirectory);
}
return Ok(Box::new(VfsHandle::Directory {
vfs: self.vfs.clone(),
path: full_path,
}));
}
let mut flags = OpenFlags::new();
if opts.read {
flags = flags.read();
}
if opts.write {
flags = flags.write();
}
match opts.intent {
OpenIntent::Open => {}
OpenIntent::Create => {
flags = flags.create().exclusive();
}
OpenIntent::OpenOrCreate => {
flags = flags.create();
}
OpenIntent::OverwriteOrCreate => {
flags = flags.create().truncate();
}
OpenIntent::Truncate => {
flags = flags.truncate();
}
}
if opts.non_directory && self.vfs.exists(&full_path) {
let stat = self.vfs.stat(&full_path).map_err(map_error)?;
if stat.is_dir {
return Err(SmbError::IsDirectory);
}
}
let file = self
.vfs
.open_file(&full_path, &flags)
.map_err(map_error)?;
Ok(Box::new(VfsHandle::File {
file: Mutex::new(file),
path: full_path,
vfs: self.vfs.clone(),
}))
}
async fn unlink(&self, path: &SmbPath) -> Result<(), SmbError> {
let full_path = resolve_path(&self.root, path);
if self.vfs.exists(&full_path) {
let stat = self.vfs.stat(&full_path).map_err(map_error)?;
if stat.is_dir {
return self.vfs.remove_dir(&full_path).map_err(map_error);
}
}
self.vfs.remove_file(&full_path).map_err(map_error)
}
async fn rename(&self, from: &SmbPath, to: &SmbPath) -> Result<(), SmbError> {
let from_path = resolve_path(&self.root, from);
let to_path = resolve_path(&self.root, to);
if self.vfs.exists(&to_path) {
return Err(SmbError::Exists);
}
self.vfs.rename(&from_path, &to_path).map_err(map_error)
}
fn capabilities(&self) -> BackendCapabilities {
BackendCapabilities {
is_read_only: self.read_only,
case_sensitive: true,
}
}
}
enum VfsHandle {
File {
file: Mutex<Box<dyn super::VfsFile + Send>>,
path: PathBuf,
vfs: Arc<dyn VfsBackend>,
},
Directory {
vfs: Arc<dyn VfsBackend>,
path: PathBuf,
},
}
#[async_trait]
impl Handle for VfsHandle {
async fn read(&self, offset: u64, len: u32) -> Result<Bytes, SmbError> {
match self {
Self::File { file, .. } => {
let mut file = file.lock().unwrap();
file.seek(std::io::SeekFrom::Start(offset))
.map_err(vfs_error_to_io)?;
let mut buf = vec![0u8; len as usize];
let n = file.read(&mut buf).map_err(map_error)?;
buf.truncate(n);
Ok(Bytes::from(buf))
}
Self::Directory { .. } => Err(SmbError::NotSupported),
}
}
async fn write(&self, offset: u64, data: &[u8]) -> Result<u32, SmbError> {
match self {
Self::File { file, .. } => {
let mut file = file.lock().unwrap();
file.seek(std::io::SeekFrom::Start(offset))
.map_err(vfs_error_to_io)?;
let n = file.write(data).map_err(map_error)?;
Ok(n as u32)
}
Self::Directory { .. } => Err(SmbError::NotSupported),
}
}
async fn flush(&self) -> Result<(), SmbError> {
match self {
Self::File { file, .. } => {
let mut file = file.lock().unwrap();
file.flush().map_err(map_error)
}
Self::Directory { .. } => Ok(()),
}
}
async fn stat(&self) -> Result<FileInfo, SmbError> {
match self {
Self::File { file, path, .. } => {
let mut f = file.lock().unwrap();
let vfs_stat = f.stat().map_err(map_error)?;
Ok(vfs_stat_to_file_info(&vfs_stat, "", path))
}
Self::Directory { vfs, path } => {
let vfs_stat = vfs.stat(path).map_err(map_error)?;
Ok(vfs_stat_to_file_info(&vfs_stat, "", path))
}
}
}
async fn set_times(&self, times: FileTimes) -> Result<(), SmbError> {
let (vfs, path) = match self {
Self::File { path, vfs, .. } => (vfs, path),
Self::Directory { vfs, path } => (vfs, path),
};
let mut stat = VfsStat::new();
if let Some(t) = times.last_write_time {
stat.mtime = filetime_to_systemtime(t);
}
if let Some(t) = times.last_access_time {
stat.atime = filetime_to_systemtime(t);
}
vfs.set_stat(path, &stat).map_err(map_error)
}
async fn truncate(&self, len: u64) -> Result<(), SmbError> {
match self {
Self::File { file, .. } => {
let mut file = file.lock().unwrap();
file.set_len(len).map_err(map_error)
}
Self::Directory { .. } => Err(SmbError::NotSupported),
}
}
async fn list_dir(&self, _pattern: Option<&str>) -> Result<Vec<DirEntry>, SmbError> {
match self {
Self::File { .. } => Err(SmbError::NotADirectory),
Self::Directory { vfs, path } => {
let entries = vfs.read_dir(path).map_err(map_error)?;
let result = entries
.into_iter()
.map(|entry| {
let info = vfs_stat_to_file_info(&entry.stat, &entry.name, path);
DirEntry { info }
})
.collect();
Ok(result)
}
}
}
async fn close(self: Box<Self>) -> Result<(), SmbError> {
Ok(())
}
}
fn filetime_to_systemtime(ft: u64) -> SystemTime {
if ft < FILETIME_OFFSET {
return SystemTime::UNIX_EPOCH;
}
let delta_secs = (ft - FILETIME_OFFSET) / 10_000_000;
let delta_ns = ((ft - FILETIME_OFFSET) % 10_000_000) as u32 * 100;
SystemTime::UNIX_EPOCH
+ std::time::Duration::new(delta_secs, delta_ns)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use smb_server::{Share, SmbServer, Access};
use crate::vfs::local_fs::LocalFs;
use super::*;
#[test]
fn test_resolve_path_root() {
let root = PathBuf::from("/srv/share");
let smb = SmbPath::root();
assert_eq!(resolve_path(&root, &smb), root);
}
#[test]
fn test_resolve_path_components() {
let root = PathBuf::from("/srv/share");
let smb: SmbPath = "dir\\sub\\file.txt".parse().unwrap();
let expected = PathBuf::from("/srv/share/dir/sub/file.txt");
assert_eq!(resolve_path(&root, &smb), expected);
}
#[test]
fn test_system_time_to_filetime() {
let epoch = SystemTime::UNIX_EPOCH;
let ft = system_time_to_filetime(epoch);
assert_eq!(ft, FILETIME_OFFSET);
}
#[test]
fn test_filetime_roundtrip() {
let now = SystemTime::now();
let ft = system_time_to_filetime(now);
let back = filetime_to_systemtime(ft);
let diff = if now > back {
now.duration_since(back).unwrap()
} else {
back.duration_since(now).unwrap()
};
assert!(diff.as_millis() < 100);
}
#[test]
fn test_map_errors() {
assert!(matches!(
map_error(VfsError::NotFound("x".into())),
SmbError::NotFound
));
assert!(matches!(
map_error(VfsError::AlreadyExists("x".into())),
SmbError::Exists
));
assert!(matches!(
map_error(VfsError::PermissionDenied("x".into())),
SmbError::AccessDenied
));
assert!(matches!(
map_error(VfsError::NotEmpty("x".into())),
SmbError::NotEmpty
));
assert!(matches!(
map_error(VfsError::NotADirectory("x".into())),
SmbError::NotADirectory
));
assert!(matches!(
map_error(VfsError::IsADirectory("x".into())),
SmbError::IsDirectory
));
}
#[test]
fn test_vfs_share_backend_creation() {
let vfs = Box::new(LocalFs::new());
let root = PathBuf::from("/tmp");
let backend = VfsShareBackend::new(vfs, root);
assert!(!backend.capabilities().is_read_only);
}
#[tokio::test]
async fn test_open_nonexistent_file() {
let vfs = Box::new(LocalFs::new());
let root = PathBuf::from("/nonexistent");
let backend = VfsShareBackend::new(vfs, root);
let smb_path: SmbPath = "missing.txt".parse().unwrap();
let opts = OpenOptions {
read: true,
write: false,
intent: OpenIntent::Open,
directory: false,
non_directory: false,
delete_on_close: false,
};
let result = backend.open(&smb_path, opts).await;
assert!(matches!(result, Err(SmbError::NotFound)));
}
#[test]
fn test_rejects_dotdot() {
assert!("a\\..\\b".parse::<SmbPath>().is_err());
}
#[test]
fn test_rejects_forbidden_chars() {
for bad in ["a<b", "a>b", "a:b", "a\"b", "a|b", "a?b", "a*b"] {
assert!(bad.parse::<SmbPath>().is_err());
}
}
}