SMB Server Phase 2: VFS backend build fix + integration test
- 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:
@@ -0,0 +1,539 @@
|
||||
use super::open_flags::OpenFlags;
|
||||
use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsStat};
|
||||
use smb2::ClientConfig;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
const SMB_TIMEOUT_SECS: u64 = 30;
|
||||
const FILETIME_TO_UNIX_SECS: u64 = 11_644_473_600;
|
||||
|
||||
fn filetime_to_systemtime(raw: u64) -> SystemTime {
|
||||
let secs = raw / 10_000_000;
|
||||
if secs > FILETIME_TO_UNIX_SECS {
|
||||
UNIX_EPOCH + Duration::from_secs(secs - FILETIME_TO_UNIX_SECS)
|
||||
} else {
|
||||
UNIX_EPOCH
|
||||
}
|
||||
}
|
||||
|
||||
fn map_smb_error(e: smb2::Error) -> VfsError {
|
||||
match e.kind() {
|
||||
smb2::ErrorKind::NotFound => VfsError::NotFound(e.to_string()),
|
||||
smb2::ErrorKind::AlreadyExists => VfsError::AlreadyExists(e.to_string()),
|
||||
smb2::ErrorKind::AccessDenied => VfsError::PermissionDenied(e.to_string()),
|
||||
smb2::ErrorKind::IsADirectory => VfsError::IsADirectory(e.to_string()),
|
||||
smb2::ErrorKind::NotADirectory => VfsError::NotADirectory(e.to_string()),
|
||||
smb2::ErrorKind::ConnectionLost | smb2::ErrorKind::TimedOut | smb2::ErrorKind::SessionExpired => {
|
||||
VfsError::Io(format!("SMB connection error: {}", e))
|
||||
}
|
||||
_ => VfsError::Io(format!("SMB error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// SMB 客户端 VFS 后端 (SMB 2/3)
|
||||
pub struct SmbVfs {
|
||||
runtime: Arc<tokio::runtime::Runtime>,
|
||||
client: Arc<Mutex<smb2::SmbClient>>,
|
||||
tree: Mutex<smb2::Tree>,
|
||||
}
|
||||
|
||||
impl SmbVfs {
|
||||
pub fn new(
|
||||
addr: &str,
|
||||
share: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<Self, VfsError> {
|
||||
let runtime = Arc::new(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|e| VfsError::Io(format!("Failed to create tokio runtime: {}", e)))?,
|
||||
);
|
||||
|
||||
let config = ClientConfig {
|
||||
addr: addr.to_string(),
|
||||
timeout: Duration::from_secs(SMB_TIMEOUT_SECS),
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
domain: String::new(),
|
||||
auto_reconnect: false,
|
||||
compression: true,
|
||||
dfs_enabled: false,
|
||||
dfs_target_overrides: std::collections::HashMap::new(),
|
||||
};
|
||||
|
||||
let (client, tree) = runtime.block_on(async {
|
||||
let mut c = smb2::SmbClient::connect(config)
|
||||
.await
|
||||
.map_err(|e| VfsError::Io(format!("SMB connect failed: {}", e)))?;
|
||||
let t = c
|
||||
.connect_share(share)
|
||||
.await
|
||||
.map_err(|e| VfsError::Io(format!("SMB connect_share failed: {}", e)))?;
|
||||
Ok::<_, VfsError>((c, t))
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
runtime,
|
||||
client: Arc::new(Mutex::new(client)),
|
||||
tree: Mutex::new(tree),
|
||||
})
|
||||
}
|
||||
|
||||
fn path_to_str(path: &Path) -> String {
|
||||
let s = path.to_string_lossy().to_string();
|
||||
s.trim_start_matches('/').to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for SmbVfs {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
runtime: self.runtime.clone(),
|
||||
client: self.client.clone(),
|
||||
tree: Mutex::new(self.tree.lock().unwrap().clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsBackend for SmbVfs {
|
||||
fn clone_boxed(&self) -> Box<dyn VfsBackend> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn read_dir(&self, path: &Path) -> Result<Vec<VfsDirEntry>, VfsError> {
|
||||
let smb_path = Self::path_to_str(path);
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let entries = self
|
||||
.runtime
|
||||
.block_on(client.list_directory(&mut *tree, &smb_path))
|
||||
.map_err(map_smb_error)?;
|
||||
|
||||
Ok(entries
|
||||
.into_iter()
|
||||
.filter(|e| e.name != "." && e.name != "..")
|
||||
.map(|e| VfsDirEntry {
|
||||
name: e.name,
|
||||
long_name: String::new(),
|
||||
stat: VfsStat {
|
||||
size: e.size,
|
||||
mode: if e.is_directory { 0o755 } else { 0o644 },
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
atime: filetime_to_systemtime(0),
|
||||
mtime: filetime_to_systemtime(e.modified.0),
|
||||
is_dir: e.is_directory,
|
||||
is_symlink: false,
|
||||
},
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn open_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
flags: &OpenFlags,
|
||||
) -> Result<Box<dyn VfsFile>, VfsError> {
|
||||
let smb_path = Self::path_to_str(path);
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
|
||||
if flags.write || flags.create || flags.truncate {
|
||||
Ok(Box::new(SmbVfsFile {
|
||||
runtime: self.runtime.clone(),
|
||||
client: self.client.clone(),
|
||||
tree: tree.clone(),
|
||||
path: smb_path,
|
||||
mode: FileMode::Write,
|
||||
position: 0,
|
||||
write_buf: Vec::new(),
|
||||
data: Vec::new(),
|
||||
size: 0,
|
||||
}))
|
||||
} else {
|
||||
let data = self
|
||||
.runtime
|
||||
.block_on(client.read_file(&mut *tree, &smb_path))
|
||||
.map_err(map_smb_error)?;
|
||||
let size = data.len() as u64;
|
||||
Ok(Box::new(SmbVfsFile {
|
||||
runtime: self.runtime.clone(),
|
||||
client: self.client.clone(),
|
||||
tree: tree.clone(),
|
||||
path: smb_path,
|
||||
mode: FileMode::Read,
|
||||
position: 0,
|
||||
write_buf: Vec::new(),
|
||||
data,
|
||||
size,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn stat(&self, path: &Path) -> Result<VfsStat, VfsError> {
|
||||
let smb_path = Self::path_to_str(path);
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let info = self
|
||||
.runtime
|
||||
.block_on(client.stat(&mut *tree, &smb_path))
|
||||
.map_err(map_smb_error)?;
|
||||
|
||||
Ok(VfsStat {
|
||||
size: info.size,
|
||||
mode: if info.is_directory { 0o755 } else { 0o644 },
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
atime: filetime_to_systemtime(info.accessed.0),
|
||||
mtime: filetime_to_systemtime(info.modified.0),
|
||||
is_dir: info.is_directory,
|
||||
is_symlink: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn lstat(&self, path: &Path) -> Result<VfsStat, VfsError> {
|
||||
self.stat(path)
|
||||
}
|
||||
|
||||
fn create_dir(&self, path: &Path, _mode: u32) -> Result<(), VfsError> {
|
||||
let smb_path = Self::path_to_str(path);
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
self.runtime
|
||||
.block_on(client.create_directory(&mut *tree, &smb_path))
|
||||
.map_err(map_smb_error)
|
||||
}
|
||||
|
||||
fn create_dir_all(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
|
||||
let mut current = path.to_path_buf();
|
||||
let mut stack = Vec::new();
|
||||
while let Some(parent) = current.parent() {
|
||||
if parent.as_os_str().is_empty() || parent == Path::new("/") {
|
||||
break;
|
||||
}
|
||||
stack.push(parent.to_path_buf());
|
||||
current = parent.to_path_buf();
|
||||
}
|
||||
for dir in stack.into_iter().rev() {
|
||||
if self.stat(&dir).is_err() {
|
||||
self.create_dir(&dir, mode)?;
|
||||
}
|
||||
}
|
||||
if self.stat(path).is_err() {
|
||||
self.create_dir(path, mode)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
|
||||
let smb_path = Self::path_to_str(path);
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
self.runtime
|
||||
.block_on(client.delete_directory(&mut *tree, &smb_path))
|
||||
.map_err(map_smb_error)
|
||||
}
|
||||
|
||||
fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
|
||||
let smb_path = Self::path_to_str(path);
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
self.runtime
|
||||
.block_on(client.delete_file(&mut *tree, &smb_path))
|
||||
.map_err(map_smb_error)
|
||||
}
|
||||
|
||||
fn rename(&self, from: &Path, to: &Path) -> Result<(), VfsError> {
|
||||
let smb_from = Self::path_to_str(from);
|
||||
let smb_to = Self::path_to_str(to);
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
self.runtime
|
||||
.block_on(client.rename(&mut *tree, &smb_from, &smb_to))
|
||||
.map_err(map_smb_error)
|
||||
}
|
||||
|
||||
fn set_stat(&self, _path: &Path, _stat: &VfsStat) -> Result<(), VfsError> {
|
||||
Err(VfsError::Unsupported("SMB set_stat".to_string()))
|
||||
}
|
||||
|
||||
fn read_link(&self, _path: &Path) -> Result<PathBuf, VfsError> {
|
||||
Err(VfsError::Unsupported("SMB read_link".to_string()))
|
||||
}
|
||||
|
||||
fn create_symlink(&self, _target: &Path, _link: &Path) -> Result<(), VfsError> {
|
||||
Err(VfsError::Unsupported("SMB create_symlink".to_string()))
|
||||
}
|
||||
|
||||
fn real_path(&self, path: &Path) -> Result<PathBuf, VfsError> {
|
||||
let smb_path = Self::path_to_str(path);
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let _info = self
|
||||
.runtime
|
||||
.block_on(client.stat(&mut *tree, &smb_path))
|
||||
.map_err(map_smb_error)?;
|
||||
Ok(path.to_path_buf())
|
||||
}
|
||||
|
||||
fn exists(&self, path: &Path) -> bool {
|
||||
let smb_path = Self::path_to_str(path);
|
||||
let mut client = match self.client.lock() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let mut tree = match self.tree.lock() {
|
||||
Ok(t) => t,
|
||||
Err(_) => return false,
|
||||
};
|
||||
self.runtime
|
||||
.block_on(client.stat(&mut *tree, &smb_path))
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn hard_link(&self, _original: &Path, _link: &Path) -> Result<(), VfsError> {
|
||||
Err(VfsError::Unsupported("SMB hard_link".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
enum FileMode {
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
|
||||
struct SmbVfsFile {
|
||||
runtime: Arc<tokio::runtime::Runtime>,
|
||||
client: Arc<Mutex<smb2::SmbClient>>,
|
||||
tree: smb2::Tree,
|
||||
path: String,
|
||||
mode: FileMode,
|
||||
position: u64,
|
||||
write_buf: Vec<u8>,
|
||||
data: Vec<u8>,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl SmbVfsFile {
|
||||
fn ensure_data_loaded(&mut self) -> Result<(), VfsError> {
|
||||
if self.data.is_empty() && self.size > 0 {
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let data = self
|
||||
.runtime
|
||||
.block_on(client.read_file(&mut self.tree, &self.path))
|
||||
.map_err(map_smb_error)?;
|
||||
self.size = data.len() as u64;
|
||||
self.data = data;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsFile for SmbVfsFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize, VfsError> {
|
||||
self.ensure_data_loaded()?;
|
||||
if self.position >= self.size {
|
||||
return Ok(0);
|
||||
}
|
||||
let start = self.position as usize;
|
||||
let available = self.size as usize - start;
|
||||
let to_copy = std::cmp::min(buf.len(), available);
|
||||
buf[..to_copy].copy_from_slice(&self.data[start..start + to_copy]);
|
||||
self.position += to_copy as u64;
|
||||
Ok(to_copy)
|
||||
}
|
||||
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize, VfsError> {
|
||||
self.write_buf.extend_from_slice(buf);
|
||||
self.position += buf.len() as u64;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> Result<u64, VfsError> {
|
||||
match pos {
|
||||
std::io::SeekFrom::Start(offset) => {
|
||||
self.position = offset;
|
||||
Ok(offset)
|
||||
}
|
||||
std::io::SeekFrom::End(offset) => {
|
||||
let new_pos = if offset >= 0 {
|
||||
self.size + offset as u64
|
||||
} else {
|
||||
self.size.saturating_sub((-offset) as u64)
|
||||
};
|
||||
self.position = new_pos;
|
||||
Ok(new_pos)
|
||||
}
|
||||
std::io::SeekFrom::Current(offset) => {
|
||||
let new_pos = if offset >= 0 {
|
||||
self.position + offset as u64
|
||||
} else {
|
||||
self.position.saturating_sub((-offset) as u64)
|
||||
};
|
||||
self.position = new_pos;
|
||||
Ok(new_pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), VfsError> {
|
||||
if let FileMode::Write = self.mode {
|
||||
if !self.write_buf.is_empty() {
|
||||
let data = std::mem::take(&mut self.write_buf);
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
self.runtime
|
||||
.block_on(client.write_file(&mut self.tree, &self.path, &data))
|
||||
.map_err(map_smb_error)?;
|
||||
self.size = data.len() as u64;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stat(&mut self) -> Result<VfsStat, VfsError> {
|
||||
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||
let info = self
|
||||
.runtime
|
||||
.block_on(client.stat(&mut self.tree, &self.path))
|
||||
.map_err(map_smb_error)?;
|
||||
Ok(VfsStat {
|
||||
size: info.size,
|
||||
mode: if info.is_directory { 0o755 } else { 0o644 },
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
atime: filetime_to_systemtime(info.accessed.0),
|
||||
mtime: filetime_to_systemtime(info.modified.0),
|
||||
is_dir: info.is_directory,
|
||||
is_symlink: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_len(&mut self, _size: u64) -> Result<(), VfsError> {
|
||||
Err(VfsError::Unsupported("SMB set_len".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SmbVfsFile {
|
||||
fn drop(&mut self) {
|
||||
if let FileMode::Write = self.mode {
|
||||
if !self.write_buf.is_empty() {
|
||||
let data = std::mem::take(&mut self.write_buf);
|
||||
if let Ok(mut client) = self.client.lock() {
|
||||
let _ = self
|
||||
.runtime
|
||||
.block_on(client.write_file(&mut self.tree, &self.path, &data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_filetime_conversion() {
|
||||
let raw: u64 = 133604700000000000;
|
||||
let st = filetime_to_systemtime(raw);
|
||||
assert!(st > UNIX_EPOCH);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_to_str() {
|
||||
assert_eq!(SmbVfs::path_to_str(Path::new("foo/bar.txt")), "foo/bar.txt");
|
||||
assert_eq!(SmbVfs::path_to_str(Path::new("/foo/bar.txt")), "foo/bar.txt");
|
||||
assert_eq!(SmbVfs::path_to_str(Path::new("")), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_mapping_invalid_data() {
|
||||
let err = smb2::Error::invalid_data("test");
|
||||
let mapped = map_smb_error(err);
|
||||
match mapped {
|
||||
VfsError::Io(_) => {}
|
||||
_ => panic!("Expected Io, got {:?}", mapped),
|
||||
}
|
||||
}
|
||||
|
||||
/// Integration test: requires Docker Samba container on port 10445.
|
||||
/// Run with: docker compose -f vendor/smb2/tests/docker/internal/docker-compose.yml up -d smb-guest
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_smb_vfs_list_root() {
|
||||
let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap();
|
||||
let entries = vfs.read_dir(Path::new("/")).unwrap();
|
||||
assert!(!entries.is_empty(), "Expected at least . and ..");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_smb_vfs_write_read_file() {
|
||||
let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap();
|
||||
|
||||
let content = b"Hello SMB VFS!";
|
||||
let path = Path::new("/smb_vfs_test.txt");
|
||||
|
||||
// Write
|
||||
{
|
||||
let flags = OpenFlags::new().write().create().truncate();
|
||||
let mut file = vfs.open_file(path, &flags).unwrap();
|
||||
file.write(content).unwrap();
|
||||
file.flush().unwrap();
|
||||
}
|
||||
|
||||
// Read back
|
||||
let flags = OpenFlags::new().read();
|
||||
let mut file = vfs.open_file(path, &flags).unwrap();
|
||||
let mut buf = vec![0u8; 1024];
|
||||
let n = file.read(&mut buf).unwrap();
|
||||
assert_eq!(&buf[..n], content);
|
||||
|
||||
// Stat
|
||||
let stat = vfs.stat(path).unwrap();
|
||||
assert_eq!(stat.size, content.len() as u64);
|
||||
|
||||
// Cleanup
|
||||
vfs.remove_file(path).unwrap();
|
||||
assert!(!vfs.exists(path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_smb_vfs_create_remove_dir() {
|
||||
let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap();
|
||||
let dir_path = Path::new("/smb_vfs_test_dir");
|
||||
|
||||
vfs.create_dir(dir_path, 0o755).unwrap();
|
||||
assert!(vfs.exists(dir_path));
|
||||
|
||||
vfs.remove_dir(dir_path).unwrap();
|
||||
assert!(!vfs.exists(dir_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_smb_vfs_rename_file() {
|
||||
let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap();
|
||||
let src = Path::new("/rename_src.txt");
|
||||
let dst = Path::new("/rename_dst.txt");
|
||||
|
||||
// Create source file
|
||||
let flags = OpenFlags::new().write().create().truncate();
|
||||
{
|
||||
let mut file = vfs.open_file(src, &flags).unwrap();
|
||||
file.write(b"rename test").unwrap();
|
||||
file.flush().unwrap();
|
||||
}
|
||||
|
||||
// Rename
|
||||
vfs.rename(src, dst).unwrap();
|
||||
assert!(!vfs.exists(src));
|
||||
assert!(vfs.exists(dst));
|
||||
|
||||
// Cleanup
|
||||
vfs.remove_file(dst).unwrap();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user