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:
@@ -1,4 +1,5 @@
|
||||
pub mod render;
|
||||
pub mod smb_server;
|
||||
pub mod test;
|
||||
|
||||
use clap::Subcommand;
|
||||
@@ -9,12 +10,15 @@ pub enum ToolsCommands {
|
||||
Render(render::RenderCommand),
|
||||
#[command(flatten)]
|
||||
Test(test::TestCommand),
|
||||
#[command(flatten)]
|
||||
SmbServer(smb_server::SmbServerCommand),
|
||||
}
|
||||
|
||||
pub async fn handle_tools_command(cmd: ToolsCommands) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
ToolsCommands::Render(c) => render::handle_render_command(c)?,
|
||||
ToolsCommands::Test(c) => test::handle_test_command(c)?,
|
||||
ToolsCommands::SmbServer(c) => smb_server::handle_smb_server_command(c).await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
71
markbase-core/src/cli/tools/smb_server.rs
Normal file
71
markbase-core/src/cli/tools/smb_server.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum SmbServerCommand {
|
||||
#[command(name = "smb-start")]
|
||||
Start {
|
||||
#[arg(short, long, default_value = "4445")]
|
||||
port: u16,
|
||||
|
||||
#[arg(short, long, default_value = "/Users/accusys/momentry/var/sftpgo/data/demo")]
|
||||
root: String,
|
||||
|
||||
#[arg(short, long, default_value = "markbase")]
|
||||
share_name: String,
|
||||
|
||||
#[arg(long)]
|
||||
read_only: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn handle_smb_server_command(cmd: SmbServerCommand) -> anyhow::Result<()> {
|
||||
#[cfg(feature = "smb-server")]
|
||||
{
|
||||
match cmd {
|
||||
SmbServerCommand::Start { port, root, share_name, read_only } => {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use smb_server::{Access, Share, SmbServer};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info")),
|
||||
)
|
||||
.try_init();
|
||||
|
||||
let addr: std::net::SocketAddr =
|
||||
format!("0.0.0.0:{}", port).parse()?;
|
||||
let root_path = PathBuf::from(&root);
|
||||
|
||||
let vfs = Box::new(crate::vfs::local_fs::LocalFs::new());
|
||||
let backend = crate::vfs::smb_server_backend::VfsShareBackend::new(vfs, root_path)
|
||||
.read_only(read_only);
|
||||
|
||||
let share = Share::new(&share_name, backend)
|
||||
.user("demo", Access::ReadWrite);
|
||||
|
||||
let server = SmbServer::builder()
|
||||
.listen(addr)
|
||||
.user("demo", "demo123")
|
||||
.share(share)
|
||||
.build()?;
|
||||
|
||||
log::info!("SMB server listening on {}", addr);
|
||||
log::info!("Share '{}' at root: {}", share_name, root);
|
||||
log::info!("User: demo / demo123");
|
||||
|
||||
server.serve().await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smb-server"))]
|
||||
{
|
||||
let _ = cmd;
|
||||
anyhow::bail!("SMB server support not enabled. Build with --features smb-server");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// 参考OpenSSH scp.c源码
|
||||
|
||||
use crate::vfs::open_flags::OpenFlags;
|
||||
use crate::vfs::{VfsBackend, VfsFile, VfsStat};
|
||||
use crate::vfs::{VfsBackend, VfsStat};
|
||||
use anyhow::{anyhow, Result};
|
||||
use log::{debug, info, warn};
|
||||
use std::io::{BufRead, Read, Write};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
pub mod local_fs;
|
||||
pub mod open_flags;
|
||||
pub mod s3_fs;
|
||||
pub mod smb_fs;
|
||||
#[cfg(feature = "smb-server")]
|
||||
pub mod smb_server_backend;
|
||||
pub mod util;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -81,7 +84,7 @@ pub struct VfsDirEntry {
|
||||
}
|
||||
|
||||
/// 打开文件的抽象
|
||||
pub trait VfsFile {
|
||||
pub trait VfsFile: Send {
|
||||
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>;
|
||||
|
||||
539
markbase-core/src/vfs/smb_fs.rs
Normal file
539
markbase-core/src/vfs/smb_fs.rs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
437
markbase-core/src/vfs/smb_server_backend.rs
Normal file
437
markbase-core/src/vfs/smb_server_backend.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::vfs::open_flags::OpenFlags;
|
||||
use crate::vfs::{VfsBackend, VfsDirEntry, VfsStat, VfsFile};
|
||||
use crate::vfs::{VfsBackend, VfsDirEntry, VfsStat};
|
||||
use crate::ssh_server::upload_hook::UploadHook;
|
||||
use bytes::{Buf, Bytes};
|
||||
use dav_server::davpath::DavPath;
|
||||
|
||||
Reference in New Issue
Block a user