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 remove_dir_all_recursive(sftp: &S::Sftp, path: &Path) -> RemoteResult<()> {
94 let entries = sftp.readdir(path).map_err(|e| {
95 error!("Failed to list directory {}: {e}", path.display());
96 RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e)
97 })?;
98 for entry in &entries {
99 let entry_path = entry.path();
100 if entry.is_dir() {
101 Self::remove_dir_all_recursive(sftp, entry_path)?;
102 } else {
103 sftp.unlink(entry_path).map_err(|e| {
104 error!("Failed to remove file {}: {e}", entry_path.display());
105 RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e)
106 })?;
107 }
108 }
109 sftp.rmdir(path).map_err(|e| {
110 error!("Failed to remove directory {}: {e}", path.display());
111 RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e)
112 })
113 }
114
115 fn copy_recursive(sftp: &S::Sftp, src: &Path, dest: &Path) -> RemoteResult<()> {
117 let src_file = sftp.stat(src).map_err(|e| {
118 error!("Failed to stat {}: {e}", src.display());
119 RemoteError::new_ex(RemoteErrorType::NoSuchFileOrDirectory, e)
120 })?;
121
122 if src_file.is_dir() {
123 let mode = src_file
125 .metadata()
126 .mode
127 .map(|m| u32::from(m) as i32)
128 .unwrap_or(0o755);
129 sftp.mkdir(dest, mode).map_err(|e| {
130 error!("Failed to create directory {}: {e}", dest.display());
131 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
132 })?;
133 let entries = sftp.readdir(src).map_err(|e| {
135 error!("Failed to list directory {}: {e}", src.display());
136 RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e)
137 })?;
138 for entry in &entries {
139 let name = entry.path().file_name().ok_or_else(|| {
140 RemoteError::new_ex(
141 RemoteErrorType::BadFile,
142 format!("entry has no file name: {}", entry.path().display()),
143 )
144 })?;
145 let child_dest = dest.join(name);
146 Self::copy_recursive(sftp, entry.path(), &child_dest)?;
147 }
148 } else {
149 let mode = src_file
151 .metadata()
152 .mode
153 .map(|m| u32::from(m) as i32)
154 .unwrap_or(0o644);
155 let mut reader = sftp.open_read(src).map_err(|e| {
156 error!("Failed to open {} for reading: {e}", src.display());
157 RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e)
158 })?;
159 let mut writer = sftp
160 .open_write(dest, WriteMode::Truncate, mode)
161 .map_err(|e| {
162 error!("Failed to open {} for writing: {e}", dest.display());
163 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
164 })?;
165 let mut buffer = [0u8; 65535];
166 loop {
167 let bytes_read = reader
168 .read(&mut buffer)
169 .map_err(|e| RemoteError::new_ex(RemoteErrorType::IoError, e))?;
170 if bytes_read == 0 {
171 break;
172 }
173 writer
174 .write_all(&buffer[..bytes_read])
175 .map_err(|e| RemoteError::new_ex(RemoteErrorType::IoError, e))?;
176 }
177 writer
178 .flush()
179 .map_err(|e| RemoteError::new_ex(RemoteErrorType::IoError, e))?;
180 }
181
182 Ok(())
183 }
184
185 fn check_connection(&mut self) -> RemoteResult<()> {
187 if self.is_connected() {
188 Ok(())
189 } else {
190 Err(RemoteError::new(RemoteErrorType::NotConnected))
191 }
192 }
193}
194
195impl<S> RemoteFs for SftpFs<S>
196where
197 S: SshSession,
198{
199 fn connect(&mut self) -> RemoteResult<Welcome> {
200 debug!("Initializing SFTP connection...");
201 let session = S::connect(&self.opts)?;
202 debug!("Getting SFTP client...");
204 let sftp = match session.sftp() {
205 Ok(s) => s,
206 Err(err) => {
207 error!("Could not get sftp client: {err}");
208 return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
209 }
210 };
211 debug!("Getting working directory...");
213 self.wrkdir = sftp.realpath(Path::new(".")).map_err(|err| {
214 error!("Could not resolve working directory: {err}");
215 RemoteError::new_ex(RemoteErrorType::ProtocolError, err)
216 })?;
217 self.session = Some(session);
218 self.sftp = Some(sftp);
219 let banner = self.session.as_ref().unwrap().banner()?;
220 debug!(
221 "Connection established: '{}'; working directory {}",
222 banner.as_deref().unwrap_or(""),
223 self.wrkdir.display()
224 );
225 Ok(Welcome::default().banner(banner))
226 }
227
228 fn disconnect(&mut self) -> RemoteResult<()> {
229 debug!("Disconnecting from remote...");
230 if let Some(session) = self.session.as_ref() {
231 self.sftp = None;
233 match session.disconnect() {
235 Ok(_) => {
236 self.session = None;
238 Ok(())
239 }
240 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::ConnectionError, err)),
241 }
242 } else {
243 Err(RemoteError::new(RemoteErrorType::NotConnected))
244 }
245 }
246
247 fn is_connected(&mut self) -> bool {
248 self.session
249 .as_ref()
250 .map(|x| x.authenticated().unwrap_or_default())
251 .unwrap_or_default()
252 }
253
254 fn pwd(&mut self) -> RemoteResult<PathBuf> {
255 self.check_connection()?;
256 Ok(self.wrkdir.clone())
257 }
258
259 fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
260 self.check_connection()?;
261 let dir = path_utils::absolutize(self.wrkdir.as_path(), dir);
262 match self.stat(dir.as_path()) {
264 Err(err) => Err(err),
265 Ok(file) if file.is_dir() => {
266 self.wrkdir = dir;
267 debug!("Changed working directory to {}", self.wrkdir.display());
268 Ok(self.wrkdir.clone())
269 }
270 Ok(_) => Err(RemoteError::new_ex(
271 RemoteErrorType::BadFile,
272 "expected directory, got file",
273 )),
274 }
275 }
276
277 fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
278 if let Some(sftp) = self.sftp.as_ref() {
279 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
280 debug!("Reading directory content of {}", path.display());
281 match sftp.readdir(path.as_path()) {
282 Err(err) => Err(RemoteError::new_ex(RemoteErrorType::StatFailed, err)),
283 Ok(files) => Ok(files),
284 }
285 } else {
286 Err(RemoteError::new(RemoteErrorType::NotConnected))
287 }
288 }
289
290 fn stat(&mut self, path: &Path) -> RemoteResult<File> {
291 if let Some(sftp) = self.sftp.as_ref() {
292 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
293 debug!("Collecting metadata for {}", path.display());
294 sftp.stat(path.as_path()).map_err(|e| {
295 error!("Stat failed: {e}");
296 RemoteError::new_ex(RemoteErrorType::NoSuchFileOrDirectory, e)
297 })
298 } else {
299 Err(RemoteError::new(RemoteErrorType::NotConnected))
300 }
301 }
302
303 fn setstat(&mut self, path: &Path, metadata: Metadata) -> RemoteResult<()> {
304 if let Some(sftp) = self.sftp.as_ref() {
305 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
306 debug!("Setting metadata for {}", path.display());
307 sftp.setstat(path.as_path(), metadata)
308 .map(|_| ())
309 .map_err(|e| {
310 error!("Setstat failed: {e}");
311 RemoteError::new_ex(RemoteErrorType::StatFailed, e)
312 })
313 } else {
314 Err(RemoteError::new(RemoteErrorType::NotConnected))
315 }
316 }
317
318 fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
319 match self.stat(path) {
320 Ok(_) => Ok(true),
321 Err(RemoteError {
322 kind: RemoteErrorType::NoSuchFileOrDirectory,
323 ..
324 }) => Ok(false),
325 Err(err) => Err(err),
326 }
327 }
328
329 fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
330 if let Some(sftp) = self.sftp.as_ref() {
331 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
332 debug!("Remove file {}", path.display());
333 sftp.unlink(path.as_path()).map_err(|e| {
334 error!("Remove failed: {e}");
335 RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e)
336 })
337 } else {
338 Err(RemoteError::new(RemoteErrorType::NotConnected))
339 }
340 }
341
342 fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
343 if let Some(sftp) = self.sftp.as_ref() {
344 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
345 debug!("Remove dir {}", path.display());
346 sftp.rmdir(path.as_path()).map_err(|e| {
347 error!("Remove failed: {e}");
348 RemoteError::new_ex(RemoteErrorType::CouldNotRemoveFile, e)
349 })
350 } else {
351 Err(RemoteError::new(RemoteErrorType::NotConnected))
352 }
353 }
354
355 fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> {
356 self.check_connection()?;
357 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
358 if !self.exists(path.as_path()).ok().unwrap_or(false) {
359 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
360 }
361 debug!("Removing directory {} recursively", path.display());
362 let sftp = self.sftp.as_ref().unwrap();
363 Self::remove_dir_all_recursive(sftp, &path)
364 }
365
366 fn create_dir(&mut self, path: &Path, mode: UnixPex) -> RemoteResult<()> {
367 self.check_connection()?;
368 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
369 debug!(
371 "Creating directory {} (mode: {:o})",
372 path.display(),
373 u32::from(mode)
374 );
375 if self.exists(path.as_path())? {
376 error!("directory {} already exists", path.display());
377 return Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists));
378 }
379 self.sftp
380 .as_ref()
381 .unwrap()
382 .mkdir(path.as_path(), u32::from(mode) as i32)
383 .map_err(|e| {
384 error!("Create dir failed: {e}");
385 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
386 })
387 }
388
389 fn symlink(&mut self, path: &Path, target: &Path) -> RemoteResult<()> {
390 self.check_connection()?;
391 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
392 debug!(
394 "Creating symlink at {} pointing to {}",
395 path.display(),
396 target.display()
397 );
398 if !self.exists(target)? {
399 error!("target {} doesn't exist", target.display());
400 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
401 }
402 self.sftp
403 .as_ref()
404 .unwrap()
405 .symlink(target, path.as_path())
406 .map_err(|e| {
407 error!("Symlink failed: {e}");
408 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
409 })
410 }
411
412 fn copy(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
413 self.check_connection()?;
414 let src = path_utils::absolutize(self.wrkdir.as_path(), src);
415 if !self.exists(src.as_path()).ok().unwrap_or(false) {
416 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
417 }
418 let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
419 debug!("Copying {} to {}", src.display(), dest.display());
420 let sftp = self.sftp.as_ref().unwrap();
421 Self::copy_recursive(sftp, &src, &dest)
422 }
423
424 fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
425 self.check_connection()?;
426 let src = path_utils::absolutize(self.wrkdir.as_path(), src);
427 if !self.exists(src.as_path()).ok().unwrap_or(false) {
429 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
430 }
431 let dest = path_utils::absolutize(self.wrkdir.as_path(), dest);
432 debug!("Moving {} to {}", src.display(), dest.display());
433 self.sftp
434 .as_ref()
435 .unwrap()
436 .rename(src.as_path(), dest.as_path())
437 .map_err(|e| {
438 error!("Move failed: {e}",);
439 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
440 })
441 }
442
443 fn exec(&mut self, cmd: &str) -> RemoteResult<(u32, String)> {
444 self.check_connection()?;
445 debug!(r#"Executing command "{cmd}""#);
446
447 self.session
448 .as_mut()
449 .unwrap()
450 .cmd_at(cmd, self.wrkdir.as_path())
451 }
452
453 fn append(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream> {
454 if let Some(sftp) = self.sftp.as_ref() {
455 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
456 debug!("Opening file at {} for appending", path.display());
457 let mode = metadata.mode.map(|x| u32::from(x) as i32).unwrap_or(0o644);
458 sftp.open_write(path.as_path(), WriteMode::Append, mode)
459 .map_err(|e| {
460 error!("Append failed: {e}",);
461 RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e)
462 })
463 } else {
464 Err(RemoteError::new(RemoteErrorType::NotConnected))
465 }
466 }
467
468 fn create(&mut self, path: &Path, metadata: &Metadata) -> RemoteResult<WriteStream> {
469 if let Some(sftp) = self.sftp.as_ref() {
470 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
471 debug!("Creating file at {}", path.display());
472 let mode = metadata.mode.map(|x| u32::from(x) as i32).unwrap_or(0o644);
473 sftp.open_write(path.as_path(), WriteMode::Truncate, mode)
474 .map_err(|e| {
475 error!("Create failed: {e}",);
476 RemoteError::new_ex(RemoteErrorType::FileCreateDenied, e)
477 })
478 } else {
479 Err(RemoteError::new(RemoteErrorType::NotConnected))
480 }
481 }
482
483 fn open(&mut self, path: &Path) -> RemoteResult<ReadStream> {
484 self.check_connection()?;
485 let path = path_utils::absolutize(self.wrkdir.as_path(), path);
486 if !self.exists(path.as_path()).ok().unwrap_or(false) {
488 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
489 }
490 debug!("Opening file at {}", path.display());
491 self.sftp
492 .as_ref()
493 .unwrap()
494 .open_read(path.as_path())
495 .map_err(|e| {
496 error!("Open failed: {e}",);
497 RemoteError::new_ex(RemoteErrorType::CouldNotOpenFile, e)
498 })
499 }
500
501 fn append_file(
504 &mut self,
505 path: &Path,
506 metadata: &Metadata,
507 mut reader: Box<dyn Read + Send>,
508 ) -> RemoteResult<u64> {
509 if self.is_connected() {
510 let mut stream = self.append(path, metadata)?;
511 trace!("Opened remote file");
512 let mut bytes: usize = 0;
513 let transfer_size = metadata.size as usize;
514 let mut buffer: [u8; 65535] = [0; 65535];
515 while bytes < transfer_size {
516 let bytes_read = reader.read(&mut buffer).map_err(|e| {
517 error!("Failed to read from file: {e}",);
518 RemoteError::new_ex(RemoteErrorType::IoError, e)
519 })?;
520 let mut delta = 0;
521 while delta < bytes_read {
522 delta += stream.write(&buffer[delta..bytes_read]).map_err(|e| {
523 error!("Failed to write to stream: {e}",);
524 RemoteError::new_ex(RemoteErrorType::IoError, e)
525 })?;
526 }
527 bytes += bytes_read;
528 }
529 self.on_written(stream)?;
530 trace!("Written {bytes} bytes to destination",);
531 Ok(bytes as u64)
532 } else {
533 Err(RemoteError::new(RemoteErrorType::NotConnected))
534 }
535 }
536
537 fn create_file(
538 &mut self,
539 path: &Path,
540 metadata: &Metadata,
541 mut reader: Box<dyn std::io::Read + Send>,
542 ) -> RemoteResult<u64> {
543 if self.is_connected() {
544 let mut stream = self.create(path, metadata)?;
545 trace!("Opened remote file");
546 let mut bytes: usize = 0;
547 let transfer_size = metadata.size as usize;
548 let mut buffer: [u8; 65535] = [0; 65535];
549 while bytes < transfer_size {
550 let bytes_read = reader.read(&mut buffer).map_err(|e| {
551 error!("Failed to read from file: {e}",);
552 RemoteError::new_ex(RemoteErrorType::IoError, e)
553 })?;
554 let mut delta = 0;
555 while delta < bytes_read {
556 delta += stream.write(&buffer[delta..bytes_read]).map_err(|e| {
557 error!("Failed to write to stream: {e}",);
558 RemoteError::new_ex(RemoteErrorType::IoError, e)
559 })?;
560 }
561 bytes += bytes_read;
562 }
563 stream.flush().map_err(|e| {
564 error!("Failed to flush stream: {e}");
565 RemoteError::new_ex(RemoteErrorType::IoError, e)
566 })?;
567 self.on_written(stream)?;
568 trace!("Written {bytes} bytes to destination",);
569 Ok(bytes as u64)
570 } else {
571 Err(RemoteError::new(RemoteErrorType::NotConnected))
572 }
573 }
574
575 fn open_file(&mut self, src: &Path, mut dest: Box<dyn Write + Send>) -> RemoteResult<u64> {
576 if self.is_connected() {
577 let transfer_size = self.stat(src)?.metadata().size as usize;
578 let mut stream = self.open(src)?;
579 trace!("File opened");
580 let mut bytes: usize = 0;
581 let mut buffer: [u8; 65535] = [0; 65535];
582 while bytes < transfer_size {
583 let bytes_read = stream.read(&mut buffer).map_err(|e| {
584 error!("Failed to read from stream: {e}");
585 RemoteError::new_ex(RemoteErrorType::IoError, e)
586 })?;
587 let mut delta = 0;
588 while delta < bytes_read {
589 delta += dest.write(&buffer[delta..bytes_read]).map_err(|e| {
590 error!("Failed to write to file: {e}",);
591 RemoteError::new_ex(RemoteErrorType::IoError, e)
592 })?;
593 }
594 bytes += bytes_read;
595 }
596 self.on_read(stream)?;
597 trace!("Copied {bytes} bytes to destination",);
598 Ok(bytes as u64)
599 } else {
600 Err(RemoteError::new(RemoteErrorType::NotConnected))
601 }
602 }
603}
604
605#[cfg(test)]
606mod tests;