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