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