use libc::{c_int, c_long, c_uint, c_ulong, size_t};
use std::io::prelude::*;
use std::io::{self, ErrorKind, SeekFrom};
use std::mem;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use util;
use {raw, Error, SessionInner};
pub struct Sftp {
raw: *mut raw::LIBSSH2_SFTP,
_sess: Rc<SessionInner>,
}
pub struct File<'sftp> {
raw: *mut raw::LIBSSH2_SFTP_HANDLE,
sftp: &'sftp Sftp,
}
#[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,
sess: &Rc<SessionInner>,
) -> Result<Self, Error> {
if raw.is_null() {
Err(Error::last_error_raw(sess.raw).unwrap_or_else(Error::unknown))
} else {
Ok(Self {
raw,
_sess: Rc::clone(sess),
})
}
}
pub fn open_mode(
&self,
filename: &Path,
flags: OpenFlags,
mode: i32,
open_type: OpenType,
) -> Result<File, Error> {
let filename = try!(util::path2bytes(filename));
unsafe {
let ret = raw::libssh2_sftp_open_ex(
self.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(self.last_error())
} 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 = try!(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 = try!(util::path2bytes(filename));
self.rc(unsafe {
raw::libssh2_sftp_mkdir_ex(
self.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 = try!(util::path2bytes(filename));
self.rc(unsafe {
raw::libssh2_sftp_rmdir_ex(
self.raw,
filename.as_ptr() as *const _,
filename.len() as c_uint,
)
})
}
pub fn stat(&self, filename: &Path) -> Result<FileStat, Error> {
let filename = try!(util::path2bytes(filename));
unsafe {
let mut ret = mem::zeroed();
let rc = raw::libssh2_sftp_stat_ex(
self.raw,
filename.as_ptr() as *const _,
filename.len() as c_uint,
raw::LIBSSH2_SFTP_STAT,
&mut ret,
);
try!(self.rc(rc));
Ok(FileStat::from_raw(&ret))
}
}
pub fn lstat(&self, filename: &Path) -> Result<FileStat, Error> {
let filename = try!(util::path2bytes(filename));
unsafe {
let mut ret = mem::zeroed();
let rc = raw::libssh2_sftp_stat_ex(
self.raw,
filename.as_ptr() as *const _,
filename.len() as c_uint,
raw::LIBSSH2_SFTP_LSTAT,
&mut ret,
);
try!(self.rc(rc));
Ok(FileStat::from_raw(&ret))
}
}
pub fn setstat(&self, filename: &Path, stat: FileStat) -> Result<(), Error> {
let filename = try!(util::path2bytes(filename));
self.rc(unsafe {
let mut raw = stat.raw();
raw::libssh2_sftp_stat_ex(
self.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 = try!(util::path2bytes(path));
let target = try!(util::path2bytes(target));
self.rc(unsafe {
raw::libssh2_sftp_symlink_ex(
self.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 = try!(util::path2bytes(path));
let mut ret = Vec::<u8>::with_capacity(128);
let mut rc;
loop {
rc = unsafe {
raw::libssh2_sftp_symlink_ex(
self.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(self.last_error())
} 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 = try!(util::path2bytes(src));
let dst = try!(util::path2bytes(dst));
self.rc(unsafe {
raw::libssh2_sftp_rename_ex(
self.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 = try!(util::path2bytes(file));
self.rc(unsafe {
raw::libssh2_sftp_unlink_ex(self.raw, file.as_ptr() as *const _, file.len() as c_uint)
})
}
pub fn last_error(&self) -> Error {
let code = unsafe { raw::libssh2_sftp_last_error(self.raw) };
Error::from_errno(code as c_int)
}
pub fn rc(&self, rc: c_int) -> Result<(), Error> {
if rc == 0 {
Ok(())
} else {
Err(self.last_error())
}
}
}
impl Drop for Sftp {
fn drop(&mut self) {
unsafe { assert_eq!(raw::libssh2_sftp_shutdown(self.raw), 0) }
}
}
impl<'sftp> File<'sftp> {
unsafe fn from_raw(sftp: &'sftp Sftp, raw: *mut raw::LIBSSH2_SFTP_HANDLE) -> File<'sftp> {
File {
raw: raw,
sftp: sftp,
}
}
pub fn setstat(&mut self, stat: FileStat) -> Result<(), Error> {
self.sftp.rc(unsafe {
let mut raw = stat.raw();
raw::libssh2_sftp_fstat_ex(self.raw, &mut raw, 1)
})
}
pub fn stat(&mut self) -> Result<FileStat, Error> {
unsafe {
let mut ret = mem::zeroed();
try!(self
.sftp
.rc(raw::libssh2_sftp_fstat_ex(self.raw, &mut ret, 0)));
Ok(FileStat::from_raw(&ret))
}
}
#[allow(missing_docs)]
pub fn statvfs(&mut self) -> Result<raw::LIBSSH2_SFTP_STATVFS, Error> {
unsafe {
let mut ret = mem::zeroed();
try!(self.sftp.rc(raw::libssh2_sftp_fstatvfs(self.raw, &mut ret)));
Ok(ret)
}
}
pub fn readdir(&mut self) -> Result<(PathBuf, FileStat), Error> {
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(
self.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(self.sftp.last_error());
} 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> {
self.sftp.rc(unsafe { raw::libssh2_sftp_fsync(self.raw) })
}
}
impl<'sftp> Read for File<'sftp> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
unsafe {
let rc =
raw::libssh2_sftp_read(self.raw, buf.as_mut_ptr() as *mut _, buf.len() as size_t);
match rc {
n if n < 0 => Err(io::Error::new(ErrorKind::Other, self.sftp.last_error())),
n => Ok(n as usize),
}
}
}
}
impl<'sftp> Write for File<'sftp> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let rc = unsafe {
raw::libssh2_sftp_write(self.raw, buf.as_ptr() as *const _, buf.len() as size_t)
};
if rc < 0 {
Err(io::Error::new(ErrorKind::Other, self.sftp.last_error()))
} else {
Ok(rc as usize)
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'sftp> Seek for File<'sftp> {
fn seek(&mut self, how: SeekFrom) -> io::Result<u64> {
let next = match how {
SeekFrom::Start(pos) => pos,
SeekFrom::Current(offset) => {
let cur = unsafe { raw::libssh2_sftp_tell64(self.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)),
},
};
unsafe { raw::libssh2_sftp_seek64(self.raw, next) }
Ok(next)
}
}
impl<'sftp> Drop for File<'sftp> {
fn drop(&mut self) {
unsafe { assert_eq!(raw::libssh2_sftp_close_handle(self.raw), 0) }
}
}
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())
}