1use std::io::Cursor;
2use std::path::{Path, PathBuf};
3use std::time::SystemTime;
4
5use async_ftp::FtpStream;
6use async_trait::async_trait;
7use tokio::io::AsyncReadExt;
8
9use crate::error::{Error, Result};
10use crate::traits::RemoteFileSystem;
11use crate::types::{FileEntry, FileMeta, FtpConfig};
12
13pub struct FtpClient {
15 config: FtpConfig,
16 stream: Option<FtpStream>,
17}
18
19impl FtpClient {
20 pub fn new(config: FtpConfig) -> Self {
21 Self {
22 config,
23 stream: None,
24 }
25 }
26
27 fn stream(&mut self) -> Result<&mut FtpStream> {
28 self.stream.as_mut().ok_or(Error::NotConnected)
29 }
30
31 fn addr(&self) -> String {
32 format!("{}:{}", self.config.host, self.config.port)
33 }
34}
35
36fn parse_list_line(base: &str, line: &str) -> Option<FileEntry> {
38 let line = line.trim();
39 if line.is_empty() {
40 return None;
41 }
42
43 if line.starts_with('d')
45 || line.starts_with('-')
46 || line.starts_with('l')
47 || line.starts_with('b')
48 || line.starts_with('c')
49 {
50 let parts: Vec<&str> = line.split_whitespace().collect();
51 if parts.len() < 9 {
52 return None;
53 }
54 let name = parts[8..].join(" ");
55 if name == "." || name == ".." {
56 return None;
57 }
58 let is_dir = parts[0].starts_with('d');
59 let is_symlink = parts[0].starts_with('l');
60 let size = parts[4].parse().unwrap_or(0);
61 let path = join_ftp(base, &name);
62 return Some(FileEntry {
63 path,
64 name,
65 is_dir,
66 is_file: !is_dir && !is_symlink,
67 is_symlink,
68 size,
69 modified: None,
70 });
71 }
72
73 let parts: Vec<&str> = line.split_whitespace().collect();
76 if parts.len() >= 4 {
77 let is_dir = parts[2].eq_ignore_ascii_case("<DIR>");
78 let (size, name_idx) = if is_dir {
79 (0u64, 3)
80 } else {
81 (parts[2].parse().unwrap_or(0), 3)
82 };
83 if name_idx >= parts.len() {
84 return None;
85 }
86 let name = parts[name_idx..].join(" ");
87 if name == "." || name == ".." {
88 return None;
89 }
90 let path = join_ftp(base, &name);
91 return Some(FileEntry {
92 path,
93 name,
94 is_dir,
95 is_file: !is_dir,
96 is_symlink: false,
97 size,
98 modified: None,
99 });
100 }
101
102 None
103}
104
105fn join_ftp(base: &str, name: &str) -> PathBuf {
106 if base.is_empty() || base == "/" {
107 PathBuf::from(format!("/{name}"))
108 } else {
109 PathBuf::from(base.trim_end_matches('/')).join(name)
110 }
111}
112
113#[async_trait]
114impl RemoteFileSystem for FtpClient {
115 async fn connect(&mut self) -> Result<()> {
116 let mut stream = FtpStream::connect(self.addr().as_str()).await?;
117 stream
118 .login(&self.config.username, &self.config.password)
119 .await?;
120 let _ = self.config.passive; self.stream = Some(stream);
122 Ok(())
123 }
124
125 async fn disconnect(&mut self) -> Result<()> {
126 if let Some(mut stream) = self.stream.take() {
127 let _ = stream.quit().await;
128 }
129 Ok(())
130 }
131
132 fn is_connected(&self) -> bool {
133 self.stream.is_some()
134 }
135
136 async fn list(&mut self, path: &str) -> Result<Vec<FileEntry>> {
137 let stream = self.stream()?;
138 let pathname = if path.is_empty() || path == "/" {
139 None
140 } else {
141 Some(path)
142 };
143 let lines = stream.list(pathname).await?;
144 Ok(lines
145 .iter()
146 .filter_map(|line| parse_list_line(path, line))
147 .collect())
148 }
149
150 async fn metadata(&mut self, path: &str) -> Result<FileMeta> {
151 let stream = self.stream()?;
152 let size = stream.size(path).await?;
153 let modified = stream
154 .mdtm(path)
155 .await?
156 .map(|dt| SystemTime::from(dt));
157
158 if let Some(size) = size {
159 return Ok(FileMeta {
160 path: PathBuf::from(path),
161 is_dir: false,
162 is_file: true,
163 is_symlink: false,
164 size: size as u64,
165 modified,
166 });
167 }
168
169 match stream.list(Some(path)).await {
171 Ok(_) => Ok(FileMeta {
172 path: PathBuf::from(path),
173 is_dir: true,
174 is_file: false,
175 is_symlink: false,
176 size: 0,
177 modified,
178 }),
179 Err(err) => Err(err.into()),
180 }
181 }
182
183 async fn exists(&mut self, path: &str) -> Result<bool> {
184 match self.metadata(path).await {
185 Ok(_) => Ok(true),
186 Err(_) => Ok(false),
187 }
188 }
189
190 async fn mkdir(&mut self, path: &str) -> Result<()> {
191 self.stream()?.mkdir(path).await?;
192 Ok(())
193 }
194
195 async fn mkdir_all(&mut self, path: &str) -> Result<()> {
196 let path = path.trim_matches('/');
197 if path.is_empty() {
198 return Ok(());
199 }
200 let mut current = String::new();
201 for part in path.split('/').filter(|p| !p.is_empty()) {
202 current = if current.is_empty() {
203 format!("/{part}")
204 } else {
205 format!("{current}/{part}")
206 };
207 if !self.exists(¤t).await? {
208 self.mkdir(¤t).await?;
209 }
210 }
211 Ok(())
212 }
213
214 async fn rmdir(&mut self, path: &str) -> Result<()> {
215 self.stream()?.rmdir(path).await?;
216 Ok(())
217 }
218
219 async fn remove_file(&mut self, path: &str) -> Result<()> {
220 self.stream()?.rm(path).await?;
221 Ok(())
222 }
223
224 async fn remove(&mut self, path: &str) -> Result<()> {
225 let meta = self.metadata(path).await?;
226 if meta.is_dir {
227 let children = self.list(path).await?;
228 for child in children {
229 let child_path = child.path.to_string_lossy().to_string();
230 Box::pin(self.remove(&child_path)).await?;
231 }
232 self.rmdir(path).await
233 } else {
234 self.remove_file(path).await
235 }
236 }
237
238 async fn rename(&mut self, from: &str, to: &str) -> Result<()> {
239 self.stream()?.rename(from, to).await?;
240 Ok(())
241 }
242
243 async fn read(&mut self, path: &str) -> Result<Vec<u8>> {
244 let cursor = self.stream()?.simple_retr(path).await?;
245 Ok(cursor.into_inner())
246 }
247
248 async fn write(&mut self, path: &str, data: &[u8]) -> Result<()> {
249 let mut reader = Cursor::new(data.to_vec());
250 self.stream()?.put(path, &mut reader).await?;
251 Ok(())
252 }
253
254 async fn download(&mut self, remote: &str, local: &Path) -> Result<()> {
255 let data = self.read(remote).await?;
256 tokio::fs::write(local, data).await?;
257 Ok(())
258 }
259
260 async fn upload(&mut self, local: &Path, remote: &str) -> Result<()> {
261 let data = tokio::fs::read(local).await?;
262 self.write(remote, &data).await
263 }
264}
265
266#[allow(dead_code)]
267async fn read_data_stream<R: AsyncReadExt + Unpin>(mut reader: R) -> Result<Vec<u8>> {
268 let mut buf = Vec::new();
269 reader.read_to_end(&mut buf).await?;
270 Ok(buf)
271}