1use crate::{Error, SessionHolder, SshResult};
2use libssh_rs_sys as sys;
3use std::convert::TryInto;
4use std::ffi::{CStr, CString};
5use std::os::raw::{c_char, c_int};
6use std::sync::{Arc, Mutex, MutexGuard};
7use std::time::{Duration, SystemTime};
8use thiserror::Error;
9
10#[derive(Error, Debug, PartialEq, Eq)]
11#[error("Sftp error code {}", .0)]
12pub struct SftpError(u32);
13
14impl SftpError {
15 pub(crate) fn from_session(sftp: sys::sftp_session) -> Self {
16 let code = unsafe { sys::sftp_get_error(sftp) as u32 };
17 Self(code)
18 }
19
20 pub(crate) fn result<T>(sftp: sys::sftp_session, status: i32, res: T) -> SshResult<T> {
21 if status == sys::SSH_OK as i32 {
22 Ok(res)
23 } else {
24 Err(Error::Sftp(SftpError::from_session(sftp)))
25 }
26 }
27}
28
29pub struct Sftp {
30 pub(crate) sess: Arc<Mutex<SessionHolder>>,
31 pub(crate) sftp_inner: sys::sftp_session,
32}
33
34unsafe impl Send for Sftp {}
35
36impl Drop for Sftp {
37 fn drop(&mut self) {
38 let (_sess, sftp) = self.lock_session();
39 unsafe {
40 sys::sftp_free(sftp);
41 }
42 }
43}
44
45impl Sftp {
46 fn lock_session(&self) -> (MutexGuard<SessionHolder>, sys::sftp_session) {
47 (self.sess.lock().unwrap(), self.sftp_inner)
48 }
49
50 pub(crate) fn init(&self) -> SshResult<()> {
51 let (_sess, sftp) = self.lock_session();
52 let res = unsafe { sys::sftp_init(sftp) };
53 SftpError::result(sftp, res, ())
54 }
55
56 pub fn create_dir(&self, filename: &str, mode: sys::mode_t) -> SshResult<()> {
60 let filename = CString::new(filename)?;
61 let (_sess, sftp) = self.lock_session();
62 let res = unsafe { sys::sftp_mkdir(sftp, filename.as_ptr(), mode) };
63 SftpError::result(sftp, res, ())
64 }
65
66 pub fn canonicalize(&self, filename: &str) -> SshResult<String> {
69 let filename = CString::new(filename)?;
70 let (_sess, sftp) = self.lock_session();
71 let res = unsafe { sys::sftp_canonicalize_path(sftp, filename.as_ptr()) };
72 if res.is_null() {
73 Err(Error::Sftp(SftpError::from_session(sftp)))
74 } else {
75 let result = unsafe { CStr::from_ptr(res) }.to_string_lossy().to_string();
76 unsafe { sys::ssh_string_free_char(res) };
77 Ok(result)
78 }
79 }
80
81 pub fn chmod(&self, filename: &str, mode: sys::mode_t) -> SshResult<()> {
83 let filename = CString::new(filename)?;
84 let (_sess, sftp) = self.lock_session();
85 let res = unsafe { sys::sftp_chmod(sftp, filename.as_ptr(), mode) };
86 SftpError::result(sftp, res, ())
87 }
88
89 pub fn chown(&self, filename: &str, owner: sys::uid_t, group: sys::gid_t) -> SshResult<()> {
91 let filename = CString::new(filename)?;
92 let (_sess, sftp) = self.lock_session();
93 let res = unsafe { sys::sftp_chown(sftp, filename.as_ptr(), owner, group) };
94 SftpError::result(sftp, res, ())
95 }
96
97 pub fn read_link(&self, filename: &str) -> SshResult<String> {
99 let filename = CString::new(filename)?;
100 let (_sess, sftp) = self.lock_session();
101 let res = unsafe { sys::sftp_readlink(sftp, filename.as_ptr()) };
102 if res.is_null() {
103 Err(Error::Sftp(SftpError::from_session(sftp)))
104 } else {
105 let result = unsafe { CStr::from_ptr(res) }.to_string_lossy().to_string();
106 unsafe { sys::ssh_string_free_char(res) };
107 Ok(result)
108 }
109 }
110
111 pub fn set_metadata(&self, filename: &str, metadata: &SetAttributes) -> SshResult<()> {
113 let filename = CString::new(filename)?;
114 let (_sess, sftp) = self.lock_session();
115 let mut attributes: sys::sftp_attributes_struct = unsafe { std::mem::zeroed() };
116
117 if let Some(size) = metadata.size {
118 attributes.size = size;
119 attributes.flags |= sys::SSH_FILEXFER_ATTR_SIZE;
120 }
121
122 if let Some((uid, gid)) = metadata.uid_gid {
123 attributes.uid = uid;
124 attributes.gid = gid;
125 attributes.flags |= sys::SSH_FILEXFER_ATTR_UIDGID;
126 }
127
128 if let Some(perms) = metadata.permissions {
129 attributes.permissions = perms;
130 attributes.flags |= sys::SSH_FILEXFER_ATTR_PERMISSIONS;
131 }
132
133 if let Some((atime, mtime)) = metadata.atime_mtime {
134 attributes.atime = atime
135 .duration_since(SystemTime::UNIX_EPOCH)
136 .expect("SystemTime to always be > UNIX_EPOCH")
137 .as_secs()
138 .try_into()
139 .unwrap();
140 attributes.mtime = mtime
141 .duration_since(SystemTime::UNIX_EPOCH)
142 .expect("SystemTime to always be > UNIX_EPOCH")
143 .as_secs()
144 .try_into()
145 .unwrap();
146 attributes.flags |= sys::SSH_FILEXFER_ATTR_ACMODTIME;
147 }
148
149 let res = unsafe { sys::sftp_setstat(sftp, filename.as_ptr(), &mut attributes) };
150 SftpError::result(sftp, res, ())
151 }
152
153 pub fn metadata(&self, filename: &str) -> SshResult<Metadata> {
155 let filename = CString::new(filename)?;
156 let (_sess, sftp) = self.lock_session();
157 let attr = unsafe { sys::sftp_stat(sftp, filename.as_ptr()) };
158 if attr.is_null() {
159 Err(Error::Sftp(SftpError::from_session(sftp)))
160 } else {
161 Ok(Metadata { attr })
162 }
163 }
164
165 pub fn symlink_metadata(&self, filename: &str) -> SshResult<Metadata> {
167 let filename = CString::new(filename)?;
168 let (_sess, sftp) = self.lock_session();
169 let attr = unsafe { sys::sftp_lstat(sftp, filename.as_ptr()) };
170 if attr.is_null() {
171 Err(Error::Sftp(SftpError::from_session(sftp)))
172 } else {
173 Ok(Metadata { attr })
174 }
175 }
176
177 pub fn rename(&self, filename: &str, new_name: &str) -> SshResult<()> {
179 let filename = CString::new(filename)?;
180 let new_name = CString::new(new_name)?;
181 let (_sess, sftp) = self.lock_session();
182 let res = unsafe { sys::sftp_rename(sftp, filename.as_ptr(), new_name.as_ptr()) };
183 SftpError::result(sftp, res, ())
184 }
185
186 pub fn remove_file(&self, filename: &str) -> SshResult<()> {
188 let filename = CString::new(filename)?;
189 let (_sess, sftp) = self.lock_session();
190 let res = unsafe { sys::sftp_unlink(sftp, filename.as_ptr()) };
191 SftpError::result(sftp, res, ())
192 }
193
194 pub fn remove_dir(&self, filename: &str) -> SshResult<()> {
196 let filename = CString::new(filename)?;
197 let (_sess, sftp) = self.lock_session();
198 let res = unsafe { sys::sftp_rmdir(sftp, filename.as_ptr()) };
199 SftpError::result(sftp, res, ())
200 }
201
202 pub fn symlink(&self, target: &str, dest: &str) -> SshResult<()> {
206 let target = CString::new(target)?;
207 let dest = CString::new(dest)?;
208 let (_sess, sftp) = self.lock_session();
209 let res = unsafe { sys::sftp_symlink(sftp, target.as_ptr(), dest.as_ptr()) };
210 SftpError::result(sftp, res, ())
211 }
212
213 pub fn open(
219 &self,
220 filename: &str,
221 accesstype: OpenFlags,
222 mode: sys::mode_t,
223 ) -> SshResult<SftpFile> {
224 let filename = CString::new(filename)?;
225 let (_sess, sftp) = self.lock_session();
226 let res = unsafe { sys::sftp_open(sftp, filename.as_ptr(), accesstype.bits(), mode) };
227 if res.is_null() {
228 Err(Error::Sftp(SftpError::from_session(sftp)))
229 } else {
230 Ok(SftpFile {
231 sess: Arc::clone(&self.sess),
232 file_inner: res,
233 sftp: sftp,
234 })
235 }
236 }
237
238 pub fn open_dir(&self, filename: &str) -> SshResult<SftpDir> {
240 let filename = CString::new(filename)?;
241 let (_sess, sftp) = self.lock_session();
242 let res = unsafe { sys::sftp_opendir(sftp, filename.as_ptr()) };
243 if res.is_null() {
244 Err(Error::Sftp(SftpError::from_session(sftp)))
245 } else {
246 Ok(SftpDir {
247 sess: Arc::clone(&self.sess),
248 dir_inner: res,
249 sftp: sftp,
250 })
251 }
252 }
253
254 pub fn read_dir(&self, filename: &str) -> SshResult<Vec<Metadata>> {
259 let dir = self.open_dir(filename)?;
260 let mut res = vec![];
261 while let Some(item) = dir.read_dir() {
262 res.push(item?);
263 }
264 Ok(res)
265 }
266}
267
268pub struct SftpFile {
269 pub(crate) sess: Arc<Mutex<SessionHolder>>,
270 pub(crate) file_inner: sys::sftp_file,
271 pub(crate) sftp: sys::sftp_session,
272}
273
274unsafe impl Send for SftpFile {}
275
276impl Drop for SftpFile {
277 fn drop(&mut self) {
278 let (_sess, file) = self.lock_session();
279 unsafe {
280 sys::sftp_close(file);
281 }
282 }
283}
284
285impl SftpFile {
286 fn lock_session(&self) -> (MutexGuard<SessionHolder>, sys::sftp_file) {
287 (self.sess.lock().unwrap(), self.file_inner)
288 }
289
290 pub fn set_blocking(&self, blocking: bool) {
291 let (_sess, file) = self.lock_session();
292 if blocking {
293 unsafe { sys::sftp_file_set_blocking(file) }
294 } else {
295 unsafe { sys::sftp_file_set_nonblocking(file) }
296 }
297 }
298
299 pub fn metadata(&self) -> SshResult<Metadata> {
301 let (_sess, file) = self.lock_session();
302 let attr = unsafe { sys::sftp_fstat(file) };
303 if attr.is_null() {
304 Err(Error::Sftp(SftpError::from_session(self.sftp)))
305 } else {
306 Ok(Metadata { attr })
307 }
308 }
309}
310
311fn io_err_from_sftp(sftp: sys::sftp_session, reason: &str) -> std::io::Error {
312 use std::io::ErrorKind;
313 let res = unsafe { sys::sftp_get_error(sftp) };
314 let kind = match res as u32 {
315 sys::SSH_FX_OK => ErrorKind::Other,
316 sys::SSH_FX_EOF => ErrorKind::UnexpectedEof,
317 sys::SSH_FX_NO_SUCH_FILE => ErrorKind::NotFound,
318 sys::SSH_FX_PERMISSION_DENIED => ErrorKind::PermissionDenied,
319 sys::SSH_FX_FAILURE => ErrorKind::Other,
320 sys::SSH_FX_BAD_MESSAGE => ErrorKind::Other,
321 sys::SSH_FX_NO_CONNECTION => ErrorKind::NotConnected,
322 sys::SSH_FX_CONNECTION_LOST => ErrorKind::ConnectionReset,
323 sys::SSH_FX_OP_UNSUPPORTED => ErrorKind::Unsupported,
324 sys::SSH_FX_INVALID_HANDLE => ErrorKind::Other,
325 sys::SSH_FX_NO_SUCH_PATH => ErrorKind::NotFound,
326 sys::SSH_FX_FILE_ALREADY_EXISTS => ErrorKind::AlreadyExists,
327 sys::SSH_FX_WRITE_PROTECT => ErrorKind::Other,
328 sys::SSH_FX_NO_MEDIA => ErrorKind::Other,
329 _ => ErrorKind::Other,
330 };
331 std::io::Error::new(kind, format!("{}: sftp error code {}", reason, res))
332}
333
334impl std::io::Read for SftpFile {
335 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
336 let (_sess, file) = self.lock_session();
337
338 let res = unsafe { sys::sftp_read(file, buf.as_mut_ptr() as _, buf.len()) };
339
340 if res >= 0 {
341 Ok(res as usize)
342 } else {
343 let err = io_err_from_sftp(self.sftp, "read");
344 if err.kind() == std::io::ErrorKind::UnexpectedEof {
345 Ok(0)
346 } else {
347 Err(err)
348 }
349 }
350 }
351}
352
353impl std::io::Write for SftpFile {
354 fn flush(&mut self) -> std::io::Result<()> {
355 let (_sess, file) = self.lock_session();
356 let res = unsafe { sys::sftp_fsync(file) };
357 if res == 0 {
358 Ok(())
359 } else {
360 Err(io_err_from_sftp(self.sftp, "fsync"))
361 }
362 }
363
364 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
365 let (_sess, file) = self.lock_session();
366
367 let res = unsafe { sys::sftp_write(file, buf.as_ptr() as _, buf.len()) };
368
369 if res >= 0 {
370 Ok(res as usize)
371 } else {
372 let err = io_err_from_sftp(self.sftp, "write");
373 if err.kind() == std::io::ErrorKind::UnexpectedEof {
374 Ok(0)
375 } else {
376 Err(err)
377 }
378 }
379 }
380}
381
382impl std::io::Seek for SftpFile {
383 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
384 let (_sess, file) = self.lock_session();
385 match pos {
386 std::io::SeekFrom::Start(p) => {
387 let res = unsafe { sys::sftp_seek64(file, p) };
388 if res == 0 {
389 Ok(p)
390 } else {
391 Err(io_err_from_sftp(self.sftp, "seek"))
392 }
393 }
394 std::io::SeekFrom::End(p) => {
395 let end = self.metadata().map_err(|e| e)?.len().ok_or_else(|| {
396 std::io::Error::new(
397 std::io::ErrorKind::Other,
398 "metadata didn't return the length",
399 )
400 })?;
401 let target = if p < 0 {
402 end.saturating_sub(p.abs() as u64)
403 } else {
404 end.saturating_add(p as u64)
405 };
406 let res = unsafe { sys::sftp_seek64(file, target) };
407 if res == 0 {
408 Ok(target)
409 } else {
410 Err(io_err_from_sftp(self.sftp, "seek"))
411 }
412 }
413 std::io::SeekFrom::Current(p) => {
414 let current = unsafe { sys::sftp_tell64(file) };
415 let target = if p < 0 {
416 current.saturating_sub(p.abs() as u64)
417 } else {
418 current.saturating_add(p as u64)
419 };
420 let res = unsafe { sys::sftp_seek64(file, target) };
421 if res == 0 {
422 Ok(target)
423 } else {
424 Err(io_err_from_sftp(self.sftp, "seek"))
425 }
426 }
427 }
428 }
429
430 fn stream_position(&mut self) -> std::io::Result<u64> {
431 let (_sess, file) = self.lock_session();
432 let current = unsafe { sys::sftp_tell64(file) };
433 Ok(current)
434 }
435}
436
437#[derive(Debug, Clone, PartialEq, Eq)]
442pub struct SetAttributes {
443 pub size: Option<u64>,
445 pub uid_gid: Option<(sys::uid_t, sys::gid_t)>,
447 pub permissions: Option<u32>,
449 pub atime_mtime: Option<(SystemTime, SystemTime)>,
452}
453
454pub struct Metadata {
458 attr: sys::sftp_attributes,
459}
460
461impl Drop for Metadata {
462 fn drop(&mut self) {
463 unsafe { sys::sftp_attributes_free(self.attr) }
464 }
465}
466
467impl Metadata {
468 fn attr(&self) -> &sys::sftp_attributes_struct {
469 unsafe { &*self.attr }
470 }
471
472 pub fn len(&self) -> Option<u64> {
473 if self.attr().flags & sys::SSH_FILEXFER_ATTR_SIZE != 0 {
474 Some(self.attr().size)
475 } else {
476 None
477 }
478 }
479
480 fn name_helper(&self, name: *const c_char) -> Option<&str> {
481 if name.is_null() {
482 None
483 } else {
484 unsafe { CStr::from_ptr(name) }.to_str().ok()
485 }
486 }
487
488 pub fn name(&self) -> Option<&str> {
489 self.name_helper(self.attr().name)
490 }
491
492 pub fn long_name(&self) -> Option<&str> {
495 self.name_helper(self.attr().longname)
496 }
497
498 pub fn owner(&self) -> Option<&str> {
500 self.name_helper(self.attr().owner)
501 }
502
503 pub fn group(&self) -> Option<&str> {
505 self.name_helper(self.attr().group)
506 }
507
508 pub fn flags(&self) -> u32 {
511 self.attr().flags
512 }
513
514 pub fn uid(&self) -> Option<u32> {
516 if self.attr().flags & sys::SSH_FILEXFER_ATTR_UIDGID != 0 {
517 Some(self.attr().uid)
518 } else {
519 None
520 }
521 }
522
523 pub fn gid(&self) -> Option<u32> {
525 if self.attr().flags & sys::SSH_FILEXFER_ATTR_UIDGID != 0 {
526 Some(self.attr().gid)
527 } else {
528 None
529 }
530 }
531
532 pub fn permissions(&self) -> Option<u32> {
534 if self.attr().flags & sys::SSH_FILEXFER_ATTR_PERMISSIONS != 0 {
535 Some(self.attr().permissions)
536 } else {
537 None
538 }
539 }
540
541 pub fn file_type(&self) -> Option<FileType> {
543 if self.attr().flags & sys::SSH_FILEXFER_ATTR_PERMISSIONS != 0 {
544 Some(match self.attr().type_ as u32 {
545 sys::SSH_FILEXFER_TYPE_SPECIAL => FileType::Special,
546 sys::SSH_FILEXFER_TYPE_SYMLINK => FileType::Symlink,
547 sys::SSH_FILEXFER_TYPE_REGULAR => FileType::Regular,
548 sys::SSH_FILEXFER_TYPE_DIRECTORY => FileType::Directory,
549 sys::SSH_FILEXFER_TYPE_UNKNOWN | _ => FileType::Unknown,
550 })
551 } else {
552 None
553 }
554 }
555
556 pub fn accessed(&self) -> Option<SystemTime> {
558 let duration = if self.attr().flags & sys::SSH_FILEXFER_ATTR_ACCESSTIME != 0 {
559 Duration::from_secs(self.attr().atime64)
560 + Duration::from_nanos(
561 if self.attr().flags & sys::SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 {
562 self.attr().atime_nseconds.into()
563 } else {
564 0
565 },
566 )
567 } else if self.attr().flags & sys::SSH_FILEXFER_ATTR_ACMODTIME != 0 {
568 Duration::from_secs(self.attr().atime.into())
569 } else {
570 return None;
571 };
572 SystemTime::UNIX_EPOCH.checked_add(duration)
573 }
574
575 pub fn created(&self) -> Option<SystemTime> {
577 let duration = if self.attr().flags & sys::SSH_FILEXFER_ATTR_CREATETIME != 0 {
578 Duration::from_secs(self.attr().createtime)
579 + Duration::from_nanos(
580 if self.attr().flags & sys::SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 {
581 self.attr().createtime_nseconds.into()
582 } else {
583 0
584 },
585 )
586 } else {
587 return None;
588 };
589 SystemTime::UNIX_EPOCH.checked_add(duration)
590 }
591
592 pub fn modified(&self) -> Option<SystemTime> {
594 let duration = if self.attr().flags & sys::SSH_FILEXFER_ATTR_MODIFYTIME != 0 {
595 Duration::from_secs(self.attr().mtime64)
596 + Duration::from_nanos(
597 if self.attr().flags & sys::SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 {
598 self.attr().mtime_nseconds.into()
599 } else {
600 0
601 },
602 )
603 } else if self.attr().flags & sys::SSH_FILEXFER_ATTR_ACMODTIME != 0 {
604 Duration::from_secs(self.attr().mtime.into())
605 } else {
606 return None;
607 };
608 SystemTime::UNIX_EPOCH.checked_add(duration)
609 }
610}
611
612pub struct SftpDir {
613 pub(crate) sess: Arc<Mutex<SessionHolder>>,
614 pub(crate) dir_inner: sys::sftp_dir,
615 pub(crate) sftp: sys::sftp_session,
616}
617
618unsafe impl Send for SftpDir {}
619
620impl Drop for SftpDir {
621 fn drop(&mut self) {
622 let (_sess, dir) = self.lock_session();
623 unsafe {
624 sys::sftp_closedir(dir);
625 }
626 }
627}
628
629impl SftpDir {
630 fn lock_session(&self) -> (MutexGuard<SessionHolder>, sys::sftp_dir) {
631 (self.sess.lock().unwrap(), self.dir_inner)
632 }
633
634 pub fn read_dir(&self) -> Option<SshResult<Metadata>> {
637 let (_sess, dir) = self.lock_session();
638 let attr = unsafe { sys::sftp_readdir(self.sftp, dir) };
639 if attr.is_null() {
640 if unsafe { sys::sftp_dir_eof(dir) } == 1 {
641 None
642 } else {
643 Some(Err(Error::Sftp(SftpError::from_session(self.sftp))))
644 }
645 } else {
646 Some(Ok(Metadata { attr }))
647 }
648 }
649}
650
651#[derive(Clone, Copy, Debug, PartialEq, Eq)]
652pub enum FileType {
653 Special,
654 Symlink,
655 Regular,
656 Directory,
657 Unknown,
658}
659
660bitflags::bitflags! {
661 pub struct OpenFlags: c_int {
663 const READ_ONLY = libc::O_RDONLY;
665 const WRITE_ONLY = libc::O_WRONLY;
667 const READ_WRITE = libc::O_RDWR;
671 const CREATE = libc::O_CREAT;
673 const EXCLUSIVE = libc::O_EXCL;
675 const TRUNCATE = libc::O_TRUNC;
677 const APPEND = libc::O_APPEND;
679 const CREATE_NEW = libc::O_CREAT | libc::O_EXCL;
683 }
684}