1use std::io::{Read, Write};
6use std::path::{Path, PathBuf};
7
8use remotefs::File;
9use remotefs::fs::{
10 Metadata, ReadStream, RemoteError, RemoteErrorType, RemoteFs, RemoteResult, UnixPex, Welcome,
11 WriteStream,
12};
13
14use super::SshOpts;
15use crate::SshSession;
16use crate::ssh::backend::{Sftp as _, WriteMode};
17use crate::utils::path as path_utils;
18
19pub struct SftpFs<S>
21where
22 S: SshSession,
23{
24 session: Option<S>,
25 sftp: Option<S::Sftp>,
26 wrkdir: PathBuf,
27 opts: SshOpts,
28}
29
30#[cfg(feature = "libssh2")]
31#[cfg_attr(docsrs, doc(cfg(feature = "libssh2")))]
32impl SftpFs<super::backend::LibSsh2Session> {
33 pub fn libssh2(opts: SshOpts) -> Self {
35 Self {
36 session: None,
37 sftp: None,
38 wrkdir: PathBuf::from("/"),
39 opts,
40 }
41 }
42}
43
44#[cfg(feature = "libssh")]
45#[cfg_attr(docsrs, doc(cfg(feature = "libssh")))]
46impl SftpFs<super::backend::LibSshSession> {
47 pub fn libssh(opts: SshOpts) -> Self {
49 Self {
50 session: None,
51 sftp: None,
52 wrkdir: PathBuf::from("/"),
53 opts,
54 }
55 }
56}
57
58#[cfg(feature = "russh")]
59#[cfg_attr(docsrs, doc(cfg(feature = "russh")))]
60impl<T> SftpFs<super::backend::RusshSession<T>>
61where
62 T: russh::client::Handler + Default + Send + 'static,
63{
64 pub fn russh(opts: SshOpts, runtime: std::sync::Arc<tokio::runtime::Runtime>) -> Self {
66 let opts = opts.runtime(runtime);
67 Self {
68 session: None,
69 sftp: None,
70 wrkdir: PathBuf::from("/"),
71 opts,
72 }
73 }
74}
75
76impl<S> SftpFs<S>
77where
78 S: SshSession,
79{
80 pub fn session(&mut self) -> Option<&mut S> {
82 self.session.as_mut()
83 }
84
85 pub fn sftp(&mut self) -> Option<&mut S::Sftp> {
87 self.sftp.as_mut()
88 }
89
90 fn check_connection(&mut self) -> RemoteResult<()> {
94 if self.is_connected() {
95 Ok(())
96 } else {
97 Err(RemoteError::new(RemoteErrorType::NotConnected))
98 }
99 }
100}
101
102impl<S> RemoteFs for SftpFs<S>
103where
104 S: SshSession,
105{
106 fn connect(&mut self) -> RemoteResult<Welcome> {
107 debug!("Initializing SFTP connection...");
108 let mut session = S::connect(&self.opts)?;
109 debug!("Getting working directory...");
111 self.wrkdir = session
112 .cmd("pwd")
113 .map(|(_rc, output)| PathBuf::from(output.as_str().trim()))?;
114 debug!("Getting SFTP client...");
116 let sftp = match session.sftp() {
117 Ok(s) => s,
118 Err(err) => {
119 error!("Could not get sftp client: {err}");
120 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
121 }
122 };
123 self.session = Some(session);
124 self.sftp = Some(sftp);
125 let banner = self.session.as_ref().unwrap().banner()?;
126 debug!(
127 "Connection established: '{}'; working directory {}",
128 banner.as_deref().unwrap_or(""),
129 self.wrkdir.display()
130 );
131 Ok(Welcome::default().banner(banner))
132 }
133
134 fn disconnect(&mut self) -> RemoteResult<()> {
135 debug!("Disconnecting from remote...");
136 if let Some(session) = self.session.as_ref() {
137 self.sftp = None;
139 match session.disconnect() {
141 Ok(_) => {
142 self.session = None;
144 Ok(())
145 }
146 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, err)),
147 }
148 } else {
149 Err(RemoteError::new(RemoteErrorType::NotConnected))
150 }
151 }
152
153 fn is_connected(&mut self) -> bool {
154 self.session
155 .as_ref()
156 .map(|x| x.authenticated().unwrap_or_default())
157 .unwrap_or_default()
158 }
159
160 fn pwd(&mut self) -> RemoteResult<PathBuf> {
161 self.check_connection()?;
162 Ok(self.wrkdir.clone())
163 }
164
165 fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
166 self.check_connection()?;
167 let dir = path_utils::absolutize(self.wrkdir.as_path(), dir);
168 match self.stat(dir.as_path()) {
170 Err(err) => Err(err),
171 Ok(file) if file.is_dir() => {
172 self.wrkdir = dir;
173 debug!("Changed working directory to {}", self.wrkdir.display());
174 Ok(self.wrkdir.clone())
175 }
176 Ok(_) => Err(RemoteError::new_ex(
177 RemoteErrorType::BadFile,
178 "expected directory, got file",
179 )),
180 }
181 }
182
183 fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
184 if let Some(sftp) = self.sftp.as_ref() {
185 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
186 debug!("Reading directory content of {}", path.display());
187 match sftp.readdir(path.as_path()) {
188 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::StatFailed, err)),
189 Ok(files) => Ok(files),
190 }
191 } else {
192 Err(RemoteError::new(RemoteErrorType::NotConnected))
193 }
194 }
195
196 fn stat(&mut self, path: &Path) -> RemoteResult<File> {
197 if let Some(sftp) = self.sftp.as_ref() {
198 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
199 debug!("Collecting metadata for {}", path.display());
200 sftp.stat(path.as_path()).map_err(|e| {
201 error!("Stat failed: {e}");
202 RemoteError::new_ex(RemoteErrorType::NoSuchFileOrDirectory, e)
203 })
204 } else {
205 Err(RemoteError::new(RemoteErrorType::NotConnected))
206 }
207 }
208
209 fn setstat(&mut self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
210 if let Some(sftp) = self.sftp.as_ref() {
211 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
212 debug!("Setting metadata for {}", path.display());
213 sftp.setstat(path.as_path(), metadata)
214 .map(|_| ())
215 .map_err(|e| {
216 error!("Setstat failed: {e}");
217 RemoteError::new_ex(RemoteErrorType::StatFailed, e)
218 })
219 } else {
220 Err(RemoteError::new(RemoteErrorType::NotConnected))
221 }
222 }
223
224 fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
225 match self.stat(path) {
226 Ok(_) => Ok(true),
227 Err(RemoteError {
228 kind: RemoteErrorType::NoSuchFileOrDirectory,
229 ..
230 }) => Ok(false),
231 Err(err) => Err(err),
232 }
233 }
234
235 fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
236 if let Some(sftp) = self.sftp.as_ref() {
237 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
238 debug!("Remove file {}", path.display());
239 sftp.unlink(path.as_path()).map_err(|e| {
240 error!("Remove failed: {e}");
241 RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e)
242 })
243 } else {
244 Err(RemoteError::new(RemoteErrorType::NotConnected))
245 }
246 }
247
248 fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
249 if let Some(sftp) = self.sftp.as_ref() {
250 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
251 debug!("Remove dir {}", path.display());
252 sftp.rmdir(path.as_path()).map_err(|e| {
253 error!("Remove failed: {e}");
254 RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e)
255 })
256 } else {
257 Err(RemoteError::new(RemoteErrorType::NotConnected))
258 }
259 }
260
261 fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> {
262 self.check_connection()?;
263 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
264 if !self.exists(path.as_path()).ok().unwrap_or(false) {
265 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
266 }
267 debug!("Removing directory {} recursively", path.display());
268 match self
269 .session
270 .as_mut()
271 .unwrap()
272 .cmd(format!("rm -rf \"{}\"", path.display()))
273 {
274 Ok((0, _)) => Ok(()),
275 Ok(_) => Err(RemoteError::new(RemoteErrorType::CouldNotRemoveFile)),
276 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
277 }
278 }
279
280 fn create_dir(&mut self, path: &Path, mode: UnixPex) -> RemoteResult<()> {
281 self.check_connection()?;
282 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
283 debug!(
285 "Creating directory {} (mode: {:o})",
286 path.display(),
287 u32::from(mode)
288 );
289 if self.exists(path.as_path())? {
290 error!("directory {} already exists", path.display());
291 return Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists));
292 }
293 self.sftp
294 .as_ref()
295 .unwrap()
296 .mkdir(path.as_path(), u32::from(mode) as i32)
297 .map_err(|e| {
298 error!("Create dir failed: {e}");
299 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
300 })
301 }
302
303 fn symlink(&mut self, path: &Path, target: &Path) -> RemoteResult<()> {
304 self.check_connection()?;
305 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
306 debug!(
308 "Creating symlink at {} pointing to {}",
309 path.display(),
310 target.display()
311 );
312 if !self.exists(target)? {
313 error!("target {} doesn't exist", target.display());
314 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
315 }
316 self.sftp
317 .as_ref()
318 .unwrap()
319 .symlink(target, path.as_path())
320 .map_err(|e| {
321 error!("Symlink failed: {e}");
322 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
323 })
324 }
325
326 fn copy(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
327 self.check_connection()?;
328 let src = path_utils::absolutize(self.wrkdir.as_path(), src);
329 if !self.exists(src.as_path()).ok().unwrap_or(false) {
331 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
332 }
333 let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
334 debug!("Copying {} to {}", src.display(), dest.display());
335 match self
337 .session
338 .as_mut()
339 .unwrap()
340 .cmd(format!("cp -rf \"{}\" \"{}\"", src.display(), dest.display()).as_str())
341 {
342 Ok((0, _)) => Ok(()),
343 Ok(_) => Err(RemoteError::new_ex(
344 RemoteErrorType::FileCreateDenied,
346 format!("\"{}\"", dest.display()),
347 )),
348 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err)),
349 }
350 }
351
352 fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
353 self.check_connection()?;
354 let src = path_utils::absolutize(self.wrkdir.as_path(), src);
355 if !self.exists(src.as_path()).ok().unwrap_or(false) {
357 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
358 }
359 let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
360 debug!("Moving {} to {}", src.display(), dest.display());
361 self.sftp
362 .as_ref()
363 .unwrap()
364 .rename(src.as_path(), dest.as_path())
365 .map_err(|e| {
366 error!("Move failed: {e}",);
367 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
368 })
369 }
370
371 fn exec(&mut self, cmd: &str) -> RemoteResult<(u32, String)> {
372 self.check_connection()?;
373 debug!(r#"Executing command "{cmd}""#);
374
375 self.session
376 .as_mut()
377 .unwrap()
378 .cmd_at(cmd, self.wrkdir.as_path())
379 }
380
381 fn append(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream> {
382 if let Some(sftp) = self.sftp.as_ref() {
383 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
384 debug!("Opening file at {} for appending", path.display());
385 let mode = metadata.mode.map(|x| u32::from(x) as i32).unwrap_or(0o644);
386 sftp.open_write(path.as_path(), WriteMode::Append, mode)
387 .map_err(|e| {
388 error!("Append failed: {e}",);
389 RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e)
390 })
391 } else {
392 Err(RemoteError::new(RemoteErrorType::NotConnected))
393 }
394 }
395
396 fn create(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream> {
397 if let Some(sftp) = self.sftp.as_ref() {
398 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
399 debug!("Creating file at {}", path.display());
400 let mode = metadata.mode.map(|x| u32::from(x) as i32).unwrap_or(0o644);
401 sftp.open_write(path.as_path(), WriteMode::Truncate, mode)
402 .map_err(|e| {
403 error!("Create failed: {e}",);
404 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
405 })
406 } else {
407 Err(RemoteError::new(RemoteErrorType::NotConnected))
408 }
409 }
410
411 fn open(&mut self, path: &Path) -> RemoteResult<ReadStream> {
412 self.check_connection()?;
413 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
414 if !self.exists(path.as_path()).ok().unwrap_or(false) {
416 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
417 }
418 debug!("Opening file at {}", path.display());
419 self.sftp
420 .as_ref()
421 .unwrap()
422 .open_read(path.as_path())
423 .map_err(|e| {
424 error!("Open failed: {e}",);
425 RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e)
426 })
427 }
428
429 fn append_file(
432 &mut self,
433 path: &Path,
434 metadata: &Metadata,
435 mut reader: Box<dyn Read + Send>,
436 ) -> RemoteResult<u64> {
437 if self.is_connected() {
438 let mut stream = self.append(path, metadata)?;
439 trace!("Opened remote file");
440 let mut bytes: usize = 0;
441 let transfer_size = metadata.size as usize;
442 let mut buffer: [u8; 65535] = [0; 65535];
443 while bytes < transfer_size {
444 let bytes_read = reader.read(&mut buffer).map_err(|e| {
445 error!("Failed to read from file: {e}",);
446 RemoteError::new_ex(RemoteErrorType::IoError, e)
447 })?;
448 let mut delta = 0;
449 while delta < bytes_read {
450 delta += stream.write(&buffer[delta..bytes_read]).map_err(|e| {
451 error!("Failed to write to stream: {e}",);
452 RemoteError::new_ex(RemoteErrorType::IoError, e)
453 })?;
454 }
455 bytes += bytes_read;
456 }
457 self.on_written(stream)?;
458 trace!("Written {bytes} bytes to destination",);
459 Ok(bytes as u64)
460 } else {
461 Err(RemoteError::new(RemoteErrorType::NotConnected))
462 }
463 }
464
465 fn create_file(
466 &mut self,
467 path: &Path,
468 metadata: &Metadata,
469 mut reader: Box<dyn std::io::Read + Send>,
470 ) -> RemoteResult<u64> {
471 if self.is_connected() {
472 let mut stream = self.create(path, metadata)?;
473 trace!("Opened remote file");
474 let mut bytes: usize = 0;
475 let transfer_size = metadata.size as usize;
476 let mut buffer: [u8; 65535] = [0; 65535];
477 while bytes < transfer_size {
478 let bytes_read = reader.read(&mut buffer).map_err(|e| {
479 error!("Failed to read from file: {e}",);
480 RemoteError::new_ex(RemoteErrorType::IoError, e)
481 })?;
482 let mut delta = 0;
483 while delta < bytes_read {
484 delta += stream.write(&buffer[delta..bytes_read]).map_err(|e| {
485 error!("Failed to write to stream: {e}",);
486 RemoteError::new_ex(RemoteErrorType::IoError, e)
487 })?;
488 }
489 bytes += bytes_read;
490 }
491 stream.flush().map_err(|e| {
492 error!("Failed to flush stream: {e}");
493 RemoteError::new_ex(RemoteErrorType::IoError, e)
494 })?;
495 self.on_written(stream)?;
496 trace!("Written {bytes} bytes to destination",);
497 Ok(bytes as u64)
498 } else {
499 Err(RemoteError::new(RemoteErrorType::NotConnected))
500 }
501 }
502
503 fn open_file(&mut self, src: &Path, mut dest: Box<dyn Write + Send>) -> RemoteResult<u64> {
504 if self.is_connected() {
505 let transfer_size = self.stat(src)?.metadata().size as usize;
506 let mut stream = self.open(src)?;
507 trace!("File opened");
508 let mut bytes: usize = 0;
509 let mut buffer: [u8; 65535] = [0; 65535];
510 while bytes < transfer_size {
511 let bytes_read = stream.read(&mut buffer).map_err(|e| {
512 error!("Failed to read from stream: {e}");
513 RemoteError::new_ex(RemoteErrorType::IoError, e)
514 })?;
515 let mut delta = 0;
516 while delta < bytes_read {
517 delta += dest.write(&buffer[delta..bytes_read]).map_err(|e| {
518 error!("Failed to write to file: {e}",);
519 RemoteError::new_ex(RemoteErrorType::IoError, e)
520 })?;
521 }
522 bytes += bytes_read;
523 }
524 self.on_read(stream)?;
525 trace!("Copied {bytes} bytes to destination",);
526 Ok(bytes as u64)
527 } else {
528 Err(RemoteError::new(RemoteErrorType::NotConnected))
529 }
530 }
531}
532
533#[cfg(test)]
534mod tests;