1use std::path::{Path, PathBuf};
2use std::time::Duration;
3
4use async_trait::async_trait;
5use smb2::{ClientConfig, DirectoryEntry, FileInfo, SmbClient as RawSmb, Tree};
6
7use crate::error::{Error, Result};
8use crate::traits::RemoteFileSystem;
9use crate::types::{FileEntry, FileMeta, SmbConfig};
10
11pub struct SmbClient {
13 config: SmbConfig,
14 client: Option<RawSmb>,
15 tree: Option<Tree>,
16}
17
18impl SmbClient {
19 pub fn new(config: SmbConfig) -> Self {
20 Self {
21 config,
22 client: None,
23 tree: None,
24 }
25 }
26
27 fn addr(&self) -> String {
28 match self.config.port {
29 Some(port) => format!("{}:{}", self.config.host, port),
30 None => format!("{}:445", self.config.host),
31 }
32 }
33
34 fn require_parts(&mut self) -> Result<(&mut RawSmb, &mut Tree)> {
35 match (&mut self.client, &mut self.tree) {
36 (Some(client), Some(tree)) => Ok((client, tree)),
37 _ => Err(Error::NotConnected),
38 }
39 }
40
41 fn entry_to_file(base: &str, entry: &DirectoryEntry) -> FileEntry {
42 let name = entry.name.clone();
43 let path = if base.is_empty() || base == "/" || base == "." {
44 PathBuf::from(&name)
45 } else {
46 PathBuf::from(base.trim_end_matches(['/', '\\'])).join(&name)
47 };
48 let is_dir = entry.is_directory;
49 let _ = &entry.modified;
50 let _ = &entry.created;
51 FileEntry {
52 path,
53 name,
54 is_dir,
55 is_file: !is_dir,
56 is_symlink: false,
57 size: entry.size,
58 modified: None,
59 }
60 }
61
62 fn info_to_meta(path: &str, info: &FileInfo) -> FileMeta {
63 let _ = &info.modified;
64 let _ = &info.created;
65 let _ = &info.accessed;
66 FileMeta {
67 path: PathBuf::from(path),
68 is_dir: info.is_directory,
69 is_file: !info.is_directory,
70 is_symlink: false,
71 size: info.size,
72 modified: None,
73 }
74 }
75}
76
77fn normalize_smb_path(path: &str) -> String {
78 path.trim_matches(['/', '\\']).replace('/', "\\")
79}
80
81#[async_trait]
82impl RemoteFileSystem for SmbClient {
83 async fn connect(&mut self) -> Result<()> {
84 let mut client = RawSmb::connect(ClientConfig {
85 addr: self.addr(),
86 timeout: Duration::from_secs(30),
87 username: self.config.username.clone(),
88 password: self.config.password.clone(),
89 domain: self.config.workgroup.clone().unwrap_or_default(),
90 auto_reconnect: true,
91 compression: true,
92 dfs_enabled: false,
93 dfs_target_overrides: Default::default(),
94 })
95 .await?;
96 let tree = client.connect_share(&self.config.share).await?;
97 self.client = Some(client);
98 self.tree = Some(tree);
99 Ok(())
100 }
101
102 async fn disconnect(&mut self) -> Result<()> {
103 if let (Some(client), Some(tree)) = (self.client.as_mut(), self.tree.take()) {
104 let _ = client.disconnect_share(&tree).await;
105 }
106 self.client = None;
107 Ok(())
108 }
109
110 fn is_connected(&self) -> bool {
111 self.client.is_some() && self.tree.is_some()
112 }
113
114 async fn list(&mut self, path: &str) -> Result<Vec<FileEntry>> {
115 let norm = normalize_smb_path(path);
116 let (client, tree) = self.require_parts()?;
117 let entries = client.list_directory(tree, &norm).await?;
118 Ok(entries
119 .iter()
120 .filter(|e| e.name != "." && e.name != "..")
121 .map(|e| Self::entry_to_file(path, e))
122 .collect())
123 }
124
125 async fn metadata(&mut self, path: &str) -> Result<FileMeta> {
126 let norm = normalize_smb_path(path);
127 let (client, tree) = self.require_parts()?;
128 let st = client.stat(tree, &norm).await?;
129 Ok(Self::info_to_meta(path, &st))
130 }
131
132 async fn exists(&mut self, path: &str) -> Result<bool> {
133 match self.metadata(path).await {
134 Ok(_) => Ok(true),
135 Err(_) => Ok(false),
136 }
137 }
138
139 async fn mkdir(&mut self, path: &str) -> Result<()> {
140 let norm = normalize_smb_path(path);
141 let (client, tree) = self.require_parts()?;
142 client.create_directory(tree, &norm).await?;
143 Ok(())
144 }
145
146 async fn mkdir_all(&mut self, path: &str) -> Result<()> {
147 let norm = normalize_smb_path(path);
148 if norm.is_empty() {
149 return Ok(());
150 }
151 let mut current = String::new();
152 for part in norm.split('\\').filter(|p| !p.is_empty()) {
153 if current.is_empty() {
154 current = part.to_string();
155 } else {
156 current = format!("{current}\\{part}");
157 }
158 if !self.exists(¤t).await? {
159 self.mkdir(¤t).await?;
160 }
161 }
162 Ok(())
163 }
164
165 async fn rmdir(&mut self, path: &str) -> Result<()> {
166 let norm = normalize_smb_path(path);
167 let (client, tree) = self.require_parts()?;
168 client.delete_directory(tree, &norm).await?;
169 Ok(())
170 }
171
172 async fn remove_file(&mut self, path: &str) -> Result<()> {
173 let norm = normalize_smb_path(path);
174 let (client, tree) = self.require_parts()?;
175 client.delete_file(tree, &norm).await?;
176 Ok(())
177 }
178
179 async fn remove(&mut self, path: &str) -> Result<()> {
180 let meta = self.metadata(path).await?;
181 if meta.is_dir {
182 let children = self.list(path).await?;
183 for child in children {
184 let child_path = child.path.to_string_lossy().replace('/', "\\");
185 Box::pin(self.remove(&child_path)).await?;
186 }
187 self.rmdir(path).await
188 } else {
189 self.remove_file(path).await
190 }
191 }
192
193 async fn rename(&mut self, from: &str, to: &str) -> Result<()> {
194 let from = normalize_smb_path(from);
195 let to = normalize_smb_path(to);
196 let (client, tree) = self.require_parts()?;
197 client.rename(tree, &from, &to).await?;
198 Ok(())
199 }
200
201 async fn read(&mut self, path: &str) -> Result<Vec<u8>> {
202 let norm = normalize_smb_path(path);
203 let (client, tree) = self.require_parts()?;
204 Ok(client.read_file(tree, &norm).await?)
205 }
206
207 async fn write(&mut self, path: &str, data: &[u8]) -> Result<()> {
208 let norm = normalize_smb_path(path);
209 let (client, tree) = self.require_parts()?;
210 client.write_file(tree, &norm, data).await?;
211 Ok(())
212 }
213
214 async fn download(&mut self, remote: &str, local: &Path) -> Result<()> {
215 let data = self.read(remote).await?;
216 tokio::fs::write(local, data).await?;
217 Ok(())
218 }
219
220 async fn upload(&mut self, local: &Path, remote: &str) -> Result<()> {
221 let data = tokio::fs::read(local).await?;
222 self.write(remote, &data).await
223 }
224}