use crate::{ client::{RemoteProcess, RemoteProcessError, Session}, data::{DirEntry, Error as Failure, FileType, Request, RequestData, ResponseData}, net::TransportError, }; use derive_more::{Display, Error, From}; use std::{future::Future, path::PathBuf, pin::Pin}; /// Represents an error that can occur related to convenience functions tied to a [`Session`] #[derive(Debug, Display, Error, From)] pub enum SessionExtError { /// Occurs when the remote action fails Failure(#[error(not(source))] Failure), /// Occurs when a transport error is encountered TransportError(TransportError), /// Occurs when receiving a response that was not expected MismatchedResponse, } pub type AsyncReturn<'a, T, E = SessionExtError> = Pin> + Send + 'a>>; /// Represents metadata about some path on a remote machine pub struct Metadata { pub file_type: FileType, pub len: u64, pub readonly: bool, pub canonicalized_path: Option, pub accessed: Option, pub created: Option, pub modified: Option, } /// Provides convenience functions on top of a [`Session`] pub trait SessionExt { /// Appends to a remote file using the data from a collection of bytes fn append_file( &mut self, tenant: impl Into, path: impl Into, data: impl Into>, ) -> AsyncReturn<'_, ()>; /// Appends to a remote file using the data from a string fn append_file_text( &mut self, tenant: impl Into, path: impl Into, data: impl Into, ) -> AsyncReturn<'_, ()>; /// Copies a remote file or directory from src to dst fn copy( &mut self, tenant: impl Into, src: impl Into, dst: impl Into, ) -> AsyncReturn<'_, ()>; /// Creates a remote directory, optionally creating all parent components if specified fn create_dir( &mut self, tenant: impl Into, path: impl Into, all: bool, ) -> AsyncReturn<'_, ()>; /// Checks if a path exists on a remote machine fn exists( &mut self, tenant: impl Into, path: impl Into, ) -> AsyncReturn<'_, bool>; /// Retrieves metadata about a path on a remote machine fn metadata( &mut self, tenant: impl Into, path: impl Into, canonicalize: bool, resolve_file_type: bool, ) -> AsyncReturn<'_, Metadata>; /// Reads entries from a directory, returning a tuple of directory entries and failures fn read_dir( &mut self, tenant: impl Into, path: impl Into, depth: usize, absolute: bool, canonicalize: bool, include_root: bool, ) -> AsyncReturn<'_, (Vec, Vec)>; /// Reads a remote file as a collection of bytes fn read_file( &mut self, tenant: impl Into, path: impl Into, ) -> AsyncReturn<'_, Vec>; /// Returns a remote file as a string fn read_file_text( &mut self, tenant: impl Into, path: impl Into, ) -> AsyncReturn<'_, String>; /// Removes a remote file or directory, supporting removal of non-empty directories if /// force is true fn remove( &mut self, tenant: impl Into, path: impl Into, force: bool, ) -> AsyncReturn<'_, ()>; /// Renames a remote file or directory from src to dst fn rename( &mut self, tenant: impl Into, src: impl Into, dst: impl Into, ) -> AsyncReturn<'_, ()>; /// Spawns a process on the remote machine fn spawn( &mut self, tenant: impl Into, cmd: impl Into, args: Vec, ) -> AsyncReturn<'_, RemoteProcess, RemoteProcessError>; /// Writes a remote file with the data from a collection of bytes fn write_file( &mut self, tenant: impl Into, path: impl Into, data: impl Into>, ) -> AsyncReturn<'_, ()>; /// Writes a remote file with the data from a string fn write_file_text( &mut self, tenant: impl Into, path: impl Into, data: impl Into, ) -> AsyncReturn<'_, ()>; } macro_rules! make_body { ($self:expr, $tenant:expr, $data:expr, @ok) => { make_body!($self, $tenant, $data, |data| { if data.is_ok() { Ok(()) } else { Err(SessionExtError::MismatchedResponse) } }) }; ($self:expr, $tenant:expr, $data:expr, $and_then:expr) => {{ let req = Request::new($tenant, vec![$data]); Box::pin(async move { $self .send(req) .await .map_err(SessionExtError::from) .and_then(|res| { if res.payload.len() == 1 { Ok(res.payload.into_iter().next().unwrap()) } else { Err(SessionExtError::MismatchedResponse) } }) .and_then($and_then) }) }}; } impl SessionExt for Session { /// Appends to a remote file using the data from a collection of bytes fn append_file( &mut self, tenant: impl Into, path: impl Into, data: impl Into>, ) -> AsyncReturn<'_, ()> { make_body!( self, tenant, RequestData::FileAppend { path: path.into(), data: data.into() }, @ok ) } /// Appends to a remote file using the data from a string fn append_file_text( &mut self, tenant: impl Into, path: impl Into, data: impl Into, ) -> AsyncReturn<'_, ()> { make_body!( self, tenant, RequestData::FileAppendText { path: path.into(), text: data.into() }, @ok ) } /// Copies a remote file or directory from src to dst fn copy( &mut self, tenant: impl Into, src: impl Into, dst: impl Into, ) -> AsyncReturn<'_, ()> { make_body!( self, tenant, RequestData::Copy { src: src.into(), dst: dst.into() }, @ok ) } /// Creates a remote directory, optionally creating all parent components if specified fn create_dir( &mut self, tenant: impl Into, path: impl Into, all: bool, ) -> AsyncReturn<'_, ()> { make_body!( self, tenant, RequestData::DirCreate { path: path.into(), all }, @ok ) } /// Checks if a path exists on a remote machine fn exists( &mut self, tenant: impl Into, path: impl Into, ) -> AsyncReturn<'_, bool> { make_body!( self, tenant, RequestData::Exists { path: path.into() }, |data| match data { ResponseData::Exists(x) => Ok(x), _ => Err(SessionExtError::MismatchedResponse), } ) } /// Retrieves metadata about a path on a remote machine fn metadata( &mut self, tenant: impl Into, path: impl Into, canonicalize: bool, resolve_file_type: bool, ) -> AsyncReturn<'_, Metadata> { make_body!( self, tenant, RequestData::Metadata { path: path.into(), canonicalize, resolve_file_type }, |data| match data { ResponseData::Metadata { canonicalized_path, file_type, len, readonly, accessed, created, modified, } => Ok(Metadata { canonicalized_path, file_type, len, readonly, accessed, created, modified, }), _ => Err(SessionExtError::MismatchedResponse), } ) } /// Reads entries from a directory, returning a tuple of directory entries and failures fn read_dir( &mut self, tenant: impl Into, path: impl Into, depth: usize, absolute: bool, canonicalize: bool, include_root: bool, ) -> AsyncReturn<'_, (Vec, Vec)> { make_body!( self, tenant, RequestData::DirRead { path: path.into(), depth, absolute, canonicalize, include_root }, |data| match data { ResponseData::DirEntries { entries, errors } => Ok((entries, errors)), _ => Err(SessionExtError::MismatchedResponse), } ) } /// Reads a remote file as a collection of bytes fn read_file( &mut self, tenant: impl Into, path: impl Into, ) -> AsyncReturn<'_, Vec> { make_body!( self, tenant, RequestData::FileRead { path: path.into() }, |data| match data { ResponseData::Blob { data } => Ok(data), _ => Err(SessionExtError::MismatchedResponse), } ) } /// Returns a remote file as a string fn read_file_text( &mut self, tenant: impl Into, path: impl Into, ) -> AsyncReturn<'_, String> { make_body!( self, tenant, RequestData::FileReadText { path: path.into() }, |data| match data { ResponseData::Text { data } => Ok(data), _ => Err(SessionExtError::MismatchedResponse), } ) } /// Removes a remote file or directory, supporting removal of non-empty directories if /// force is true fn remove( &mut self, tenant: impl Into, path: impl Into, force: bool, ) -> AsyncReturn<'_, ()> { make_body!( self, tenant, RequestData::Remove { path: path.into(), force }, @ok ) } /// Renames a remote file or directory from src to dst fn rename( &mut self, tenant: impl Into, src: impl Into, dst: impl Into, ) -> AsyncReturn<'_, ()> { make_body!( self, tenant, RequestData::Rename { src: src.into(), dst: dst.into() }, @ok ) } /// Spawns a process on the remote machine fn spawn( &mut self, tenant: impl Into, cmd: impl Into, args: Vec, ) -> AsyncReturn<'_, RemoteProcess, RemoteProcessError> { let tenant = tenant.into(); let cmd = cmd.into(); Box::pin(async move { RemoteProcess::spawn(tenant, self, cmd, args).await }) } /// Writes a remote file with the data from a collection of bytes fn write_file( &mut self, tenant: impl Into, path: impl Into, data: impl Into>, ) -> AsyncReturn<'_, ()> { make_body!( self, tenant, RequestData::FileWrite { path: path.into(), data: data.into() }, @ok ) } /// Writes a remote file with the data from a string fn write_file_text( &mut self, tenant: impl Into, path: impl Into, data: impl Into, ) -> AsyncReturn<'_, ()> { make_body!( self, tenant, RequestData::FileWriteText { path: path.into(), text: data.into() }, @ok ) } }