use libc::{c_int, c_long, c_uint, c_ulong, size_t};
use parking_lot::{Mutex, MutexGuard};
use std::io::prelude::*;
use std::io::{self, ErrorKind, SeekFrom};
use std::mem;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use util;
use {raw, Error, SessionInner};
pub struct Sftp {
inner: Option<Arc<SftpInnerDropWrapper>>,
}
struct SftpInnerDropWrapper(Option<SftpInner>);
struct SftpInner {
raw: *mut raw::LIBSSH2_SFTP,
sess: Arc<Mutex<SessionInner>>,
}
unsafe impl Send for Sftp {}
unsafe impl Sync for Sftp {}
struct LockedSftp<'sftp> {
raw: *mut raw::LIBSSH2_SFTP,
sess: MutexGuard<'sftp, SessionInner>,
}
pub struct File {
inner: Option<FileInner>,
}
struct FileInner {
raw: *mut raw::LIBSSH2_SFTP_HANDLE,
sftp: Arc<SftpInnerDropWrapper>,
}
unsafe impl Send for File {}
unsafe impl Sync for File {}
struct LockedFile<'file> {
raw: *mut raw::LIBSSH2_SFTP_HANDLE,
sess: MutexGuard<'file, SessionInner>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[allow(missing_copy_implementations)]
pub struct FileStat {
pub size: Option<u64>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub perm: Option<u32>,
pub atime: Option<u64>,
pub mtime: Option<u64>,
}
pub struct FileType {
perm: c_ulong,
}
bitflags! {
pub struct OpenFlags: c_ulong {
const READ = raw::LIBSSH2_FXF_READ;
const WRITE = raw::LIBSSH2_FXF_WRITE;
const APPEND = raw::LIBSSH2_FXF_APPEND;
const CREATE = raw::LIBSSH2_FXF_CREAT;
const TRUNCATE = raw::LIBSSH2_FXF_TRUNC | Self::CREATE.bits;
const EXCLUSIVE = raw::LIBSSH2_FXF_EXCL | Self::CREATE.bits;
}
}
bitflags! {
pub struct RenameFlags: c_long {
const OVERWRITE = raw::LIBSSH2_SFTP_RENAME_OVERWRITE;
const ATOMIC = raw::LIBSSH2_SFTP_RENAME_ATOMIC;
const NATIVE = raw::LIBSSH2_SFTP_RENAME_NATIVE;
}
}
#[derive(Copy, Clone)]
pub enum OpenType {
File = raw::LIBSSH2_SFTP_OPENFILE as isize,
Dir = raw::LIBSSH2_SFTP_OPENDIR as isize,
}
impl Sftp {
pub(crate) fn from_raw_opt(
raw: *mut raw::LIBSSH2_SFTP,
err: Option<Error>,
sess: &Arc<Mutex<SessionInner>>,
) -> Result<Self, Error> {
if raw.is_null() {
Err(err.unwrap_or_else(Error::unknown))
} else {
Ok(Self {
inner: Some(Arc::new(SftpInnerDropWrapper(Some(SftpInner {
raw,
sess: Arc::clone(sess),
})))),
})
}
}
pub fn open_mode(
&self,
filename: &Path,
flags: OpenFlags,
mode: i32,
open_type: OpenType,
) -> Result<File, Error> {
let filename = util::path2bytes(filename)?;
let locked = self.lock()?;
unsafe {
let ret = raw::libssh2_sftp_open_ex(
locked.raw,
filename.as_ptr() as *const _,
filename.len() as c_uint,
flags.bits() as c_ulong,
mode as c_long,
open_type as c_int,
);
if ret.is_null() {
Err(locked.sess.last_error().unwrap_or_else(Error::unknown))
} else {
Ok(File::from_raw(self, ret))
}
}
}
pub fn open(&self, filename: &Path) -> Result<File, Error> {
self.open_mode(filename, OpenFlags::READ, 0o644, OpenType::File)
}
pub fn create(&self, filename: &Path) -> Result<File, Error> {
self.open_mode(
filename,
OpenFlags::WRITE | OpenFlags::TRUNCATE,
0o644,
OpenType::File,
)
}
pub fn opendir(&self, dirname: &Path) -> Result<File, Error> {
self.open_mode(dirname, OpenFlags::READ, 0, OpenType::Dir)
}
pub fn readdir(&self, dirname: &Path) -> Result<Vec<(PathBuf, FileStat)>, Error> {
let mut dir = self.opendir(dirname)?;
let mut ret = Vec::new();
loop {
match dir.readdir() {
Ok((filename, stat)) => {
if &*filename == Path::new(".") || &*filename == Path::new("..") {
continue;
}
ret.push((dirname.join(&filename), stat))
}
Err(ref e) if e.code() == raw::LIBSSH2_ERROR_FILE => break,
Err(e) => return Err(e),
}
}
Ok(ret)
}
pub fn mkdir(&self, filename: &Path, mode: i32) -> Result<(), Error> {
let filename = util::path2bytes(filename)?;
let locked = self.lock()?;
locked.sess.rc(unsafe {
raw::libssh2_sftp_mkdir_ex(
locked.raw,
filename.as_ptr() as *const _,
filename.len() as c_uint,
mode as c_long,
)
})
}
pub fn rmdir(&self, filename: &Path) -> Result<(), Error> {
let filename = util::path2bytes(filename)?;
let locked = self.lock()?;
locked.sess.rc(unsafe {
raw::libssh2_sftp_rmdir_ex(
locked.raw,
filename.as_ptr() as *const _,
filename.len() as c_uint,
)
})
}
pub fn stat(&self, filename: &Path) -> Result<FileStat, Error> {
let filename = util::path2bytes(filename)?;
let locked = self.lock()?;
unsafe {
let mut ret = mem::zeroed();
let rc = raw::libssh2_sftp_stat_ex(
locked.raw,
filename.as_ptr() as *const _,
filename.len() as c_uint,
raw::LIBSSH2_SFTP_STAT,
&mut ret,
);
locked.sess.rc(rc)?;
Ok(FileStat::from_raw(&ret))
}
}
pub fn lstat(&self, filename: &Path) -> Result<FileStat, Error> {
let filename = util::path2bytes(filename)?;
let locked = self.lock()?;
unsafe {
let mut ret = mem::zeroed();
let rc = raw::libssh2_sftp_stat_ex(
locked.raw,
filename.as_ptr() as *const _,
filename.len() as c_uint,
raw::LIBSSH2_SFTP_LSTAT,
&mut ret,
);
locked.sess.rc(rc)?;
Ok(FileStat::from_raw(&ret))
}
}
pub fn setstat(&self, filename: &Path, stat: FileStat) -> Result<(), Error> {
let filename = util::path2bytes(filename)?;
let locked = self.lock()?;
locked.sess.rc(unsafe {
let mut raw = stat.raw();
raw::libssh2_sftp_stat_ex(
locked.raw,
filename.as_ptr() as *const _,
filename.len() as c_uint,
raw::LIBSSH2_SFTP_SETSTAT,
&mut raw,
)
})
}
pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), Error> {
let path = util::path2bytes(path)?;
let target = util::path2bytes(target)?;
let locked = self.lock()?;
locked.sess.rc(unsafe {
raw::libssh2_sftp_symlink_ex(
locked.raw,
path.as_ptr() as *const _,
path.len() as c_uint,
target.as_ptr() as *mut _,
target.len() as c_uint,
raw::LIBSSH2_SFTP_SYMLINK,
)
})
}
pub fn readlink(&self, path: &Path) -> Result<PathBuf, Error> {
self.readlink_op(path, raw::LIBSSH2_SFTP_READLINK)
}
pub fn realpath(&self, path: &Path) -> Result<PathBuf, Error> {
self.readlink_op(path, raw::LIBSSH2_SFTP_REALPATH)
}
fn readlink_op(&self, path: &Path, op: c_int) -> Result<PathBuf, Error> {
let path = util::path2bytes(path)?;
let mut ret = Vec::<u8>::with_capacity(128);
let mut rc;
let locked = self.lock()?;
loop {
rc = unsafe {
raw::libssh2_sftp_symlink_ex(
locked.raw,
path.as_ptr() as *const _,
path.len() as c_uint,
ret.as_ptr() as *mut _,
ret.capacity() as c_uint,
op,
)
};
if rc == raw::LIBSSH2_ERROR_BUFFER_TOO_SMALL {
let cap = ret.capacity();
ret.reserve(cap);
} else {
break;
}
}
if rc < 0 {
Err(Error::from_session_error_raw(locked.sess.raw, rc))
} else {
unsafe { ret.set_len(rc as usize) }
Ok(mkpath(ret))
}
}
pub fn rename(&self, src: &Path, dst: &Path, flags: Option<RenameFlags>) -> Result<(), Error> {
let flags =
flags.unwrap_or(RenameFlags::ATOMIC | RenameFlags::OVERWRITE | RenameFlags::NATIVE);
let src = util::path2bytes(src)?;
let dst = util::path2bytes(dst)?;
let locked = self.lock()?;
locked.sess.rc(unsafe {
raw::libssh2_sftp_rename_ex(
locked.raw,
src.as_ptr() as *const _,
src.len() as c_uint,
dst.as_ptr() as *const _,
dst.len() as c_uint,
flags.bits(),
)
})
}
pub fn unlink(&self, file: &Path) -> Result<(), Error> {
let file = util::path2bytes(file)?;
let locked = self.lock()?;
locked.sess.rc(unsafe {
raw::libssh2_sftp_unlink_ex(locked.raw, file.as_ptr() as *const _, file.len() as c_uint)
})
}
fn lock(&self) -> Result<LockedSftp, Error> {
match self.inner.as_ref() {
Some(sftp_inner_drop_wrapper) => {
let sftp_inner = sftp_inner_drop_wrapper
.0
.as_ref()
.expect("Never unset until shutdown, in which case inner is also unset");
let sess = sftp_inner.sess.lock();
Ok(LockedSftp {
sess,
raw: sftp_inner.raw,
})
}
None => Err(Error::from_errno(raw::LIBSSH2_ERROR_BAD_USE)),
}
}
#[doc(hidden)]
pub fn shutdown(&mut self) -> Result<(), Error> {
match self.inner.take() {
Some(sftp_inner_arc) => {
match Arc::try_unwrap(sftp_inner_arc) {
Ok(mut sftp_inner_wrapper) => {
let sftp_inner = sftp_inner_wrapper.0.take().expect(
"We were holding an Arc<SftpInnerDropWrapper>, \
so nobody could unset this (set on creation)",
);
sftp_inner
.sess
.lock()
.rc(unsafe { raw::libssh2_sftp_shutdown(sftp_inner.raw) })?;
Ok(())
}
Err(sftp_inner_arc) => {
self.inner = Some(sftp_inner_arc);
Err(Error::from_errno(raw::LIBSSH2_ERROR_BAD_USE))
}
}
}
None => {
Err(Error::from_errno(raw::LIBSSH2_ERROR_BAD_USE))
}
}
}
}
impl Drop for SftpInnerDropWrapper {
fn drop(&mut self) {
if let Some(inner) = self.0.take() {
let sess = inner.sess.lock();
let was_blocking = sess.is_blocking();
sess.set_blocking(true);
let _shutdown_result = unsafe { raw::libssh2_sftp_shutdown(inner.raw) };
sess.set_blocking(was_blocking);
}
}
}
impl File {
unsafe fn from_raw(sftp: &Sftp, raw: *mut raw::LIBSSH2_SFTP_HANDLE) -> File {
File {
inner: Some(FileInner {
raw,
sftp: Arc::clone(
&sftp
.inner
.as_ref()
.expect("Cannot open file after sftp shutdown"),
),
}),
}
}
pub fn setstat(&mut self, stat: FileStat) -> Result<(), Error> {
let locked = self.lock()?;
locked.sess.rc(unsafe {
let mut raw = stat.raw();
raw::libssh2_sftp_fstat_ex(locked.raw, &mut raw, 1)
})
}
pub fn stat(&mut self) -> Result<FileStat, Error> {
let locked = self.lock()?;
unsafe {
let mut ret = mem::zeroed();
locked
.sess
.rc(raw::libssh2_sftp_fstat_ex(locked.raw, &mut ret, 0))?;
Ok(FileStat::from_raw(&ret))
}
}
#[allow(missing_docs)]
pub fn statvfs(&mut self) -> Result<raw::LIBSSH2_SFTP_STATVFS, Error> {
let locked = self.lock()?;
unsafe {
let mut ret = mem::zeroed();
locked
.sess
.rc(raw::libssh2_sftp_fstatvfs(locked.raw, &mut ret))?;
Ok(ret)
}
}
pub fn readdir(&mut self) -> Result<(PathBuf, FileStat), Error> {
let locked = self.lock()?;
let mut buf = Vec::<u8>::with_capacity(128);
let mut stat = unsafe { mem::zeroed() };
let mut rc;
loop {
rc = unsafe {
raw::libssh2_sftp_readdir_ex(
locked.raw,
buf.as_mut_ptr() as *mut _,
buf.capacity() as size_t,
0 as *mut _,
0,
&mut stat,
)
};
if rc == raw::LIBSSH2_ERROR_BUFFER_TOO_SMALL {
let cap = buf.capacity();
buf.reserve(cap);
} else {
break;
}
}
if rc < 0 {
return Err(Error::from_session_error_raw(locked.sess.raw, rc));
} else if rc == 0 {
return Err(Error::new(raw::LIBSSH2_ERROR_FILE, "no more files"));
} else {
unsafe {
buf.set_len(rc as usize);
}
}
Ok((mkpath(buf), FileStat::from_raw(&stat)))
}
pub fn fsync(&mut self) -> Result<(), Error> {
let locked = self.lock()?;
locked
.sess
.rc(unsafe { raw::libssh2_sftp_fsync(locked.raw) })
}
fn lock(&self) -> Result<LockedFile, Error> {
match self.inner.as_ref() {
Some(file_inner) => {
let sftp_inner = file_inner.sftp.0.as_ref().expect(
"We are holding an Arc<SftpInnerDropWrapper>, \
so nobody could unset this (set on creation)",
);
let sess = sftp_inner.sess.lock();
Ok(LockedFile {
sess,
raw: file_inner.raw,
})
}
None => Err(Error::from_errno(raw::LIBSSH2_ERROR_BAD_USE)),
}
}
#[doc(hidden)]
pub fn close(&mut self) -> Result<(), Error> {
{
let locked = self.lock()?;
Error::rc(unsafe { raw::libssh2_sftp_close_handle(locked.raw) })?;
}
self.inner = None;
Ok(())
}
}
impl Read for File {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let locked = self.lock()?;
unsafe {
let rc =
raw::libssh2_sftp_read(locked.raw, buf.as_mut_ptr() as *mut _, buf.len() as size_t);
if rc < 0 {
Err(Error::from_session_error_raw(locked.sess.raw, rc as _).into())
} else {
Ok(rc as usize)
}
}
}
}
impl Write for File {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let locked = self.lock()?;
let rc = unsafe {
raw::libssh2_sftp_write(locked.raw, buf.as_ptr() as *const _, buf.len() as size_t)
};
if rc < 0 {
Err(Error::from_session_error_raw(locked.sess.raw, rc as _).into())
} else {
Ok(rc as usize)
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Seek for File {
fn seek(&mut self, how: SeekFrom) -> io::Result<u64> {
let next = match how {
SeekFrom::Start(pos) => pos,
SeekFrom::Current(offset) => {
let locked = self.lock()?;
let cur = unsafe { raw::libssh2_sftp_tell64(locked.raw) };
(cur as i64 + offset) as u64
}
SeekFrom::End(offset) => match self.stat() {
Ok(s) => match s.size {
Some(size) => (size as i64 + offset) as u64,
None => return Err(io::Error::new(ErrorKind::Other, "no file size available")),
},
Err(e) => return Err(io::Error::new(ErrorKind::Other, e)),
},
};
let locked = self.lock()?;
unsafe { raw::libssh2_sftp_seek64(locked.raw, next) }
Ok(next)
}
}
impl Drop for File {
fn drop(&mut self) {
if let Some(file_inner) = self.inner.take() {
let sftp_inner = file_inner.sftp.0.as_ref().expect(
"We are holding an Arc<SftpInnerDropWrapper>, \
so nobody could unset this (set on creation)",
);
let sess_inner = sftp_inner.sess.lock();
let was_blocking = sess_inner.is_blocking();
sess_inner.set_blocking(true);
let _close_handle_result = unsafe { raw::libssh2_sftp_close_handle(file_inner.raw) };
sess_inner.set_blocking(was_blocking);
}
}
}
impl FileStat {
pub fn file_type(&self) -> FileType {
FileType {
perm: self.perm.unwrap_or(0) as c_ulong,
}
}
pub fn is_dir(&self) -> bool {
self.file_type().is_dir()
}
pub fn is_file(&self) -> bool {
self.file_type().is_file()
}
pub fn from_raw(raw: &raw::LIBSSH2_SFTP_ATTRIBUTES) -> FileStat {
fn val<T: Copy>(raw: &raw::LIBSSH2_SFTP_ATTRIBUTES, t: &T, flag: c_ulong) -> Option<T> {
if raw.flags & flag != 0 {
Some(*t)
} else {
None
}
}
FileStat {
size: val(raw, &raw.filesize, raw::LIBSSH2_SFTP_ATTR_SIZE),
uid: val(raw, &raw.uid, raw::LIBSSH2_SFTP_ATTR_UIDGID).map(|s| s as u32),
gid: val(raw, &raw.gid, raw::LIBSSH2_SFTP_ATTR_UIDGID).map(|s| s as u32),
perm: val(raw, &raw.permissions, raw::LIBSSH2_SFTP_ATTR_PERMISSIONS).map(|s| s as u32),
mtime: val(raw, &raw.mtime, raw::LIBSSH2_SFTP_ATTR_ACMODTIME).map(|s| s as u64),
atime: val(raw, &raw.atime, raw::LIBSSH2_SFTP_ATTR_ACMODTIME).map(|s| s as u64),
}
}
pub fn raw(&self) -> raw::LIBSSH2_SFTP_ATTRIBUTES {
fn flag<T>(o: &Option<T>, flag: c_ulong) -> c_ulong {
if o.is_some() {
flag
} else {
0
}
}
raw::LIBSSH2_SFTP_ATTRIBUTES {
flags: flag(&self.size, raw::LIBSSH2_SFTP_ATTR_SIZE)
| flag(&self.uid, raw::LIBSSH2_SFTP_ATTR_UIDGID)
| flag(&self.gid, raw::LIBSSH2_SFTP_ATTR_UIDGID)
| flag(&self.perm, raw::LIBSSH2_SFTP_ATTR_PERMISSIONS)
| flag(&self.atime, raw::LIBSSH2_SFTP_ATTR_ACMODTIME)
| flag(&self.mtime, raw::LIBSSH2_SFTP_ATTR_ACMODTIME),
filesize: self.size.unwrap_or(0),
uid: self.uid.unwrap_or(0) as c_ulong,
gid: self.gid.unwrap_or(0) as c_ulong,
permissions: self.perm.unwrap_or(0) as c_ulong,
atime: self.atime.unwrap_or(0) as c_ulong,
mtime: self.mtime.unwrap_or(0) as c_ulong,
}
}
}
impl FileType {
pub fn is_dir(&self) -> bool {
self.is(raw::LIBSSH2_SFTP_S_IFDIR)
}
pub fn is_file(&self) -> bool {
self.is(raw::LIBSSH2_SFTP_S_IFREG)
}
pub fn is_symlink(&self) -> bool {
self.is(raw::LIBSSH2_SFTP_S_IFLNK)
}
fn is(&self, perm: c_ulong) -> bool {
(self.perm & raw::LIBSSH2_SFTP_S_IFMT) == perm
}
}
#[cfg(unix)]
fn mkpath(v: Vec<u8>) -> PathBuf {
use std::ffi::OsStr;
use std::os::unix::prelude::*;
PathBuf::from(OsStr::from_bytes(&v))
}
#[cfg(windows)]
fn mkpath(v: Vec<u8>) -> PathBuf {
use std::str;
PathBuf::from(str::from_utf8(&v).unwrap())
}