1use std::path::{Path, PathBuf};
2use std::time::{Duration, SystemTime, UNIX_EPOCH};
3
4use async_ssh2_tokio::client::{AuthMethod, Client as SshClient, ServerCheckMethod};
5use async_trait::async_trait;
6use russh_sftp::client::SftpSession;
7use russh_sftp::protocol::FileType;
8use tokio::io::{AsyncReadExt, AsyncWriteExt};
9
10use crate::error::{Error, Result};
11use crate::traits::RemoteFileSystem;
12use crate::types::{FileEntry, FileMeta, SftpConfig};
13
14pub struct SftpClient {
16 config: SftpConfig,
17 ssh: Option<SshClient>,
18 sftp: Option<SftpSession>,
19}
20
21impl SftpClient {
22 pub fn new(config: SftpConfig) -> Self {
23 Self {
24 config,
25 ssh: None,
26 sftp: None,
27 }
28 }
29
30 fn auth_method(&self) -> Result<AuthMethod> {
31 if let Some(ref password) = self.config.password {
32 Ok(AuthMethod::with_password(password))
33 } else if let Some(ref key) = self.config.private_key_path {
34 Ok(AuthMethod::with_key_file(key, None))
35 } else {
36 Err(Error::Auth(
37 "SFTP requires password or private key".into(),
38 ))
39 }
40 }
41
42 fn sftp(&self) -> Result<&SftpSession> {
43 self.sftp.as_ref().ok_or(Error::NotConnected)
44 }
45
46 async fn open_sftp(ssh: &SshClient) -> Result<SftpSession> {
47 let channel = ssh.get_channel().await?;
48 channel.request_subsystem(true, "sftp").await.map_err(|e| {
49 Error::Protocol(format!("failed to request sftp subsystem: {e}"))
50 })?;
51 let sftp = SftpSession::new(channel.into_stream()).await?;
52 Ok(sftp)
53 }
54}
55
56fn file_type_flags(ft: &FileType) -> (bool, bool, bool) {
57 match ft {
58 FileType::Dir => (true, false, false),
59 FileType::Symlink => (false, false, true),
60 FileType::File => (false, true, false),
61 _ => (false, true, false),
62 }
63}
64
65fn mtime_to_system(mtime: Option<u32>) -> Option<SystemTime> {
66 mtime.map(|secs| UNIX_EPOCH + Duration::from_secs(secs as u64))
67}
68
69#[async_trait]
70impl RemoteFileSystem for SftpClient {
71 async fn connect(&mut self) -> Result<()> {
72 let auth = self.auth_method()?;
73 let ssh = SshClient::connect(
74 (self.config.host.as_str(), self.config.port),
75 &self.config.username,
76 auth,
77 ServerCheckMethod::NoCheck,
78 )
79 .await?;
80 let sftp = Self::open_sftp(&ssh).await?;
81 self.ssh = Some(ssh);
82 self.sftp = Some(sftp);
83 Ok(())
84 }
85
86 async fn disconnect(&mut self) -> Result<()> {
87 if let Some(sftp) = self.sftp.take() {
88 let _ = sftp.close().await;
89 }
90 self.ssh = None;
91 Ok(())
92 }
93
94 fn is_connected(&self) -> bool {
95 self.ssh.is_some() && self.sftp.is_some()
96 }
97
98 async fn list(&mut self, path: &str) -> Result<Vec<FileEntry>> {
99 let sftp = self.sftp()?;
100 let mut entries = Vec::new();
101 let mut dir = sftp.read_dir(path.to_string()).await?;
102 while let Some(entry) = dir.next() {
103 let name = entry.file_name();
104 if name == "." || name == ".." {
105 continue;
106 }
107 let meta = entry.metadata();
108 let (is_dir, is_file, is_symlink) = file_type_flags(&meta.file_type());
109 let child = if path.ends_with('/') {
110 format!("{path}{name}")
111 } else if path.is_empty() {
112 name.clone()
113 } else {
114 format!("{path}/{name}")
115 };
116 entries.push(FileEntry {
117 path: PathBuf::from(child),
118 name,
119 is_dir,
120 is_file,
121 is_symlink,
122 size: meta.size.unwrap_or(0),
123 modified: mtime_to_system(meta.mtime),
124 });
125 }
126 Ok(entries)
127 }
128
129 async fn metadata(&mut self, path: &str) -> Result<FileMeta> {
130 let sftp = self.sftp()?;
131 let meta = sftp.metadata(path.to_string()).await?;
132 let (is_dir, is_file, is_symlink) = file_type_flags(&meta.file_type());
133 Ok(FileMeta {
134 path: PathBuf::from(path),
135 is_dir,
136 is_file,
137 is_symlink,
138 size: meta.size.unwrap_or(0),
139 modified: mtime_to_system(meta.mtime),
140 })
141 }
142
143 async fn exists(&mut self, path: &str) -> Result<bool> {
144 Ok(self.sftp()?.try_exists(path.to_string()).await?)
145 }
146
147 async fn mkdir(&mut self, path: &str) -> Result<()> {
148 self.sftp()?.create_dir(path.to_string()).await?;
149 Ok(())
150 }
151
152 async fn mkdir_all(&mut self, path: &str) -> Result<()> {
153 let path = path.trim_matches('/');
154 if path.is_empty() {
155 return Ok(());
156 }
157 let mut current = String::new();
158 for part in path.split('/').filter(|p| !p.is_empty()) {
159 current = if current.is_empty() {
160 format!("/{part}")
161 } else {
162 format!("{current}/{part}")
163 };
164 if !self.exists(¤t).await? {
165 self.mkdir(¤t).await?;
166 }
167 }
168 Ok(())
169 }
170
171 async fn rmdir(&mut self, path: &str) -> Result<()> {
172 self.sftp()?.remove_dir(path.to_string()).await?;
173 Ok(())
174 }
175
176 async fn remove_file(&mut self, path: &str) -> Result<()> {
177 self.sftp()?.remove_file(path.to_string()).await?;
178 Ok(())
179 }
180
181 async fn remove(&mut self, path: &str) -> Result<()> {
182 let meta = self.metadata(path).await?;
183 if meta.is_dir {
184 let children = self.list(path).await?;
185 for child in children {
186 let child_path = child.path.to_string_lossy().to_string();
187 Box::pin(self.remove(&child_path)).await?;
188 }
189 self.rmdir(path).await
190 } else {
191 self.remove_file(path).await
192 }
193 }
194
195 async fn rename(&mut self, from: &str, to: &str) -> Result<()> {
196 self.sftp()?
197 .rename(from.to_string(), to.to_string())
198 .await?;
199 Ok(())
200 }
201
202 async fn read(&mut self, path: &str) -> Result<Vec<u8>> {
203 Ok(self.sftp()?.read(path.to_string()).await?)
204 }
205
206 async fn write(&mut self, path: &str, data: &[u8]) -> Result<()> {
207 self.sftp()?.write(path.to_string(), data).await?;
208 Ok(())
209 }
210
211 async fn download(&mut self, remote: &str, local: &Path) -> Result<()> {
212 let data = self.read(remote).await?;
213 tokio::fs::write(local, data).await?;
214 Ok(())
215 }
216
217 async fn upload(&mut self, local: &Path, remote: &str) -> Result<()> {
218 let data = tokio::fs::read(local).await?;
219 self.write(remote, &data).await
220 }
221}
222
223#[allow(dead_code)]
224async fn copy_async_read_to_vec<R: AsyncReadExt + Unpin>(mut reader: R) -> Result<Vec<u8>> {
225 let mut buf = Vec::new();
226 reader.read_to_end(&mut buf).await?;
227 Ok(buf)
228}
229
230#[allow(dead_code)]
231async fn copy_slice_to_async_write<W: AsyncWriteExt + Unpin>(
232 mut writer: W,
233 data: &[u8],
234) -> Result<()> {
235 writer.write_all(data).await?;
236 writer.flush().await?;
237 Ok(())
238}