use std::path::{Path, PathBuf};
use std::time::Duration;
use async_trait::async_trait;
use smb2::{ClientConfig, DirectoryEntry, FileInfo, SmbClient as RawSmb, Tree};
use crate::error::{Error, Result};
use crate::traits::RemoteFileSystem;
use crate::types::{FileEntry, FileMeta, SmbConfig};
pub struct SmbClient {
config: SmbConfig,
client: Option<RawSmb>,
tree: Option<Tree>,
}
impl SmbClient {
pub fn new(config: SmbConfig) -> Self {
Self {
config,
client: None,
tree: None,
}
}
fn addr(&self) -> String {
match self.config.port {
Some(port) => format!("{}:{}", self.config.host, port),
None => format!("{}:445", self.config.host),
}
}
fn require_parts(&mut self) -> Result<(&mut RawSmb, &mut Tree)> {
match (&mut self.client, &mut self.tree) {
(Some(client), Some(tree)) => Ok((client, tree)),
_ => Err(Error::NotConnected),
}
}
fn entry_to_file(base: &str, entry: &DirectoryEntry) -> FileEntry {
let name = entry.name.clone();
let path = if base.is_empty() || base == "/" || base == "." {
PathBuf::from(&name)
} else {
PathBuf::from(base.trim_end_matches(['/', '\\'])).join(&name)
};
let is_dir = entry.is_directory;
let _ = &entry.modified;
let _ = &entry.created;
FileEntry {
path,
name,
is_dir,
is_file: !is_dir,
is_symlink: false,
size: entry.size,
modified: None,
}
}
fn info_to_meta(path: &str, info: &FileInfo) -> FileMeta {
let _ = &info.modified;
let _ = &info.created;
let _ = &info.accessed;
FileMeta {
path: PathBuf::from(path),
is_dir: info.is_directory,
is_file: !info.is_directory,
is_symlink: false,
size: info.size,
modified: None,
}
}
}
fn normalize_smb_path(path: &str) -> String {
path.trim_matches(['/', '\\']).replace('/', "\\")
}
#[async_trait]
impl RemoteFileSystem for SmbClient {
async fn connect(&mut self) -> Result<()> {
let mut client = RawSmb::connect(ClientConfig {
addr: self.addr(),
timeout: Duration::from_secs(30),
username: self.config.username.clone(),
password: self.config.password.clone(),
domain: self.config.workgroup.clone().unwrap_or_default(),
auto_reconnect: true,
compression: true,
dfs_enabled: false,
dfs_target_overrides: Default::default(),
})
.await?;
let tree = client.connect_share(&self.config.share).await?;
self.client = Some(client);
self.tree = Some(tree);
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
if let (Some(client), Some(tree)) = (self.client.as_mut(), self.tree.take()) {
let _ = client.disconnect_share(&tree).await;
}
self.client = None;
Ok(())
}
fn is_connected(&self) -> bool {
self.client.is_some() && self.tree.is_some()
}
async fn list(&mut self, path: &str) -> Result<Vec<FileEntry>> {
let norm = normalize_smb_path(path);
let (client, tree) = self.require_parts()?;
let entries = client.list_directory(tree, &norm).await?;
Ok(entries
.iter()
.filter(|e| e.name != "." && e.name != "..")
.map(|e| Self::entry_to_file(path, e))
.collect())
}
async fn metadata(&mut self, path: &str) -> Result<FileMeta> {
let norm = normalize_smb_path(path);
let (client, tree) = self.require_parts()?;
let st = client.stat(tree, &norm).await?;
Ok(Self::info_to_meta(path, &st))
}
async fn exists(&mut self, path: &str) -> Result<bool> {
match self.metadata(path).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
async fn mkdir(&mut self, path: &str) -> Result<()> {
let norm = normalize_smb_path(path);
let (client, tree) = self.require_parts()?;
client.create_directory(tree, &norm).await?;
Ok(())
}
async fn mkdir_all(&mut self, path: &str) -> Result<()> {
let norm = normalize_smb_path(path);
if norm.is_empty() {
return Ok(());
}
let mut current = String::new();
for part in norm.split('\\').filter(|p| !p.is_empty()) {
if current.is_empty() {
current = part.to_string();
} else {
current = format!("{current}\\{part}");
}
if !self.exists(¤t).await? {
self.mkdir(¤t).await?;
}
}
Ok(())
}
async fn rmdir(&mut self, path: &str) -> Result<()> {
let norm = normalize_smb_path(path);
let (client, tree) = self.require_parts()?;
client.delete_directory(tree, &norm).await?;
Ok(())
}
async fn remove_file(&mut self, path: &str) -> Result<()> {
let norm = normalize_smb_path(path);
let (client, tree) = self.require_parts()?;
client.delete_file(tree, &norm).await?;
Ok(())
}
async fn remove(&mut self, path: &str) -> Result<()> {
let meta = self.metadata(path).await?;
if meta.is_dir {
let children = self.list(path).await?;
for child in children {
let child_path = child.path.to_string_lossy().replace('/', "\\");
Box::pin(self.remove(&child_path)).await?;
}
self.rmdir(path).await
} else {
self.remove_file(path).await
}
}
async fn rename(&mut self, from: &str, to: &str) -> Result<()> {
let from = normalize_smb_path(from);
let to = normalize_smb_path(to);
let (client, tree) = self.require_parts()?;
client.rename(tree, &from, &to).await?;
Ok(())
}
async fn read(&mut self, path: &str) -> Result<Vec<u8>> {
let norm = normalize_smb_path(path);
let (client, tree) = self.require_parts()?;
Ok(client.read_file(tree, &norm).await?)
}
async fn write(&mut self, path: &str, data: &[u8]) -> Result<()> {
let norm = normalize_smb_path(path);
let (client, tree) = self.require_parts()?;
client.write_file(tree, &norm, data).await?;
Ok(())
}
async fn download(&mut self, remote: &str, local: &Path) -> Result<()> {
let data = self.read(remote).await?;
tokio::fs::write(local, data).await?;
Ok(())
}
async fn upload(&mut self, local: &Path, remote: &str) -> Result<()> {
let data = tokio::fs::read(local).await?;
self.write(remote, &data).await
}
}