Usage: smb-start --s3 --s3-endpoint https://s3.example.com --s3-bucket mybucket --s3-access-key AKIA... --s3-secret-key secret... All SMB operations now work over S3-compatible storage. All 229 tests pass.
145 lines
4.6 KiB
Rust
145 lines
4.6 KiB
Rust
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,
|
|
|
|
#[arg(short, long, value_parser = parse_user)]
|
|
user: Vec<(String, String)>,
|
|
|
|
#[arg(long)]
|
|
s3: bool,
|
|
|
|
#[arg(long)]
|
|
s3_endpoint: Option<String>,
|
|
|
|
#[arg(long)]
|
|
s3_bucket: Option<String>,
|
|
|
|
#[arg(long)]
|
|
s3_access_key: Option<String>,
|
|
|
|
#[arg(long)]
|
|
s3_secret_key: Option<String>,
|
|
|
|
#[arg(long, default_value = "us-east-1")]
|
|
s3_region: String,
|
|
},
|
|
}
|
|
|
|
fn parse_user(s: &str) -> Result<(String, String), String> {
|
|
let parts: Vec<&str> = s.split(':').collect();
|
|
if parts.len() != 2 {
|
|
return Err(format!("Invalid user format '{}'. Expected 'name:password'", s));
|
|
}
|
|
Ok((parts[0].to_string(), parts[1].to_string()))
|
|
}
|
|
|
|
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,
|
|
user,
|
|
s3,
|
|
s3_endpoint,
|
|
s3_bucket,
|
|
s3_access_key,
|
|
s3_secret_key,
|
|
s3_region,
|
|
} => {
|
|
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<dyn crate::vfs::VfsBackend> = if s3 {
|
|
let endpoint = s3_endpoint
|
|
.ok_or_else(|| anyhow::anyhow!("--s3-endpoint required when --s3 is enabled"))?;
|
|
let bucket = s3_bucket
|
|
.ok_or_else(|| anyhow::anyhow!("--s3-bucket required when --s3 is enabled"))?;
|
|
let access_key = s3_access_key
|
|
.ok_or_else(|| anyhow::anyhow!("--s3-access-key required when --s3 is enabled"))?;
|
|
let secret_key = s3_secret_key
|
|
.ok_or_else(|| anyhow::anyhow!("--s3-secret-key required when --s3 is enabled"))?;
|
|
|
|
log::info!("S3 backend: endpoint={}, bucket={}, region={}", endpoint, bucket, s3_region);
|
|
|
|
Box::new(crate::vfs::s3_fs::S3Vfs::new(
|
|
&endpoint,
|
|
&s3_region,
|
|
&bucket,
|
|
&access_key,
|
|
&secret_key,
|
|
)?)
|
|
} else {
|
|
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 users: Vec<(String, String)> = if user.is_empty() {
|
|
vec![("demo".to_string(), "demo123".to_string())]
|
|
} else {
|
|
user
|
|
};
|
|
|
|
let mut builder = SmbServer::builder().listen(addr);
|
|
|
|
for (name, password) in &users {
|
|
builder = builder.user(name, password);
|
|
}
|
|
|
|
let mut share = Share::new(&share_name, backend);
|
|
for (name, _) in &users {
|
|
share = share.user(name, Access::ReadWrite);
|
|
}
|
|
|
|
let server = builder.share(share).build()?;
|
|
|
|
let user_list: Vec<&str> = users.iter().map(|(n, _)| n.as_str()).collect();
|
|
log::info!("SMB server listening on {}", addr);
|
|
log::info!("Share '{}' at root: {}", share_name, root);
|
|
log::info!("Users: {}", user_list.join(", "));
|
|
|
|
server.serve().await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "smb-server"))]
|
|
{
|
|
let _ = cmd;
|
|
anyhow::bail!("SMB server support not enabled. Build with --features smb-server");
|
|
}
|
|
|
|
Ok(())
|
|
}
|