Refactor a lot of commands -- need to update tests

pull/172/head
Chip Senkbeil 1 year ago
parent abac61a707
commit 6c72f4a577
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.20.0-alpha.5]
### Added
- CLI now offers the following new subcommands
- `distant fs copy` is a refactoring of `distant client action copy`
- `distant fs read` is a refactoring of `distant client action file-read`,
`distant client action file-read-text`, and `distant client action dir-read`
- `distant fs rename` is a refactoring of `distant client action rename`
- `distant fs write` is a refactoring of `distant client action file-write`,
`distant client action file-write-text`, `distant client action file-append`,
- `distant fs make-dir` is a refactoring of `distant client action dir-create`
- `distant fs remove` is a refactoring of `distant client action remove`
- `distant fs search` is a refactoring of `distant client action search`
- `distant spawn` is a refactoring of `distant client action proc-spawn`
with `distant client lsp` merged in using the `--lsp` flag
- `distant system-info` is a refactoring of `distant client action system-info`
### Changed
- CLI subcommands refactored
@ -20,6 +36,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `distant client repl` moved to `distant repl`
- `distant client shell` moved to `distant shell`
### Removed
- `distant-core` crate no longer offers the `clap` feature
## [0.20.0-alpha.4] - 2023-03-31
### Added

1
Cargo.lock generated

@ -898,7 +898,6 @@ dependencies = [
"async-trait",
"bitflags 2.0.2",
"bytes",
"clap",
"derive_more",
"distant-net",
"env_logger",

@ -32,7 +32,7 @@ clap_complete = "4.2.0"
config = { version = "0.13.3", default-features = false, features = ["toml"] }
derive_more = { version = "0.99.17", default-features = false, features = ["display", "from", "error", "is_variant"] }
dialoguer = { version = "0.10.3", default-features = false }
distant-core = { version = "=0.20.0-alpha.5", path = "distant-core", features = ["clap", "schemars"] }
distant-core = { version = "=0.20.0-alpha.5", path = "distant-core", features = ["schemars"] }
directories = "5.0.0"
flexi_logger = "0.25.3"
indoc = "2.0.1"

@ -43,7 +43,6 @@ whoami = "1.4.0"
winsplit = "0.1.0"
# Optional dependencies based on features
clap = { version = "4.2.1", features = ["derive"], optional = true }
schemars = { version = "0.8.12", optional = true }
[dev-dependencies]

@ -3,9 +3,6 @@ use serde::{Deserialize, Serialize};
use std::{io, path::PathBuf};
use strum::{AsRefStr, EnumDiscriminants, EnumIter, EnumMessage, EnumString};
#[cfg(feature = "clap")]
use strum::VariantNames;
mod capabilities;
pub use capabilities::*;
@ -15,9 +12,6 @@ pub use change::*;
mod cmd;
pub use cmd::*;
#[cfg(feature = "clap")]
mod clap_impl;
mod error;
pub use error::*;
@ -45,17 +39,6 @@ pub type ProcessId = u32;
/// Mapping of environment variables
pub type Environment = distant_net::common::Map;
/// Type alias for a vec of bytes
///
/// NOTE: This only exists to support properly parsing a Vec<u8> from an entire string
/// with clap rather than trying to parse a string as a singular u8
pub type ByteVec = Vec<u8>;
#[cfg(feature = "clap")]
fn parse_byte_vec(src: &str) -> Result<ByteVec, std::convert::Infallible> {
Ok(src.as_bytes().to_vec())
}
/// Represents a wrapper around a distant message, supporting single and batch requests
#[derive(Clone, Debug, From, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@ -143,7 +126,6 @@ impl<T: schemars::JsonSchema> DistantMsg<T> {
/// Represents the payload of a request to be performed on the remote machine
#[derive(Clone, Debug, PartialEq, Eq, EnumDiscriminants, IsVariant, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
#[strum_discriminants(derive(
AsRefStr,
strum::Display,
@ -164,14 +146,12 @@ impl<T: schemars::JsonSchema> DistantMsg<T> {
#[strum_discriminants(name(CapabilityKind))]
#[strum_discriminants(strum(serialize_all = "snake_case"))]
#[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")]
#[cfg_attr(feature = "clap", clap(rename_all = "kebab-case"))]
pub enum DistantRequestData {
/// Retrieve information about the server's capabilities
#[strum_discriminants(strum(message = "Supports retrieving capabilities"))]
Capabilities {},
/// Reads a file from the specified path on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["cat"]))]
#[strum_discriminants(strum(message = "Supports reading binary file"))]
FileRead {
/// The path to the file on the remote machine
@ -194,8 +174,9 @@ pub enum DistantRequestData {
path: PathBuf,
/// Data for server-side writing of content
#[cfg_attr(feature = "clap", clap(value_parser = parse_byte_vec))]
data: ByteVec,
#[serde(with = "serde_bytes")]
#[cfg_attr(feature = "schemars", schemars(with = "Vec<u8>"))]
data: Vec<u8>,
},
/// Writes a file using text instead of bytes, creating it if it does not exist,
@ -216,8 +197,9 @@ pub enum DistantRequestData {
path: PathBuf,
/// Data for server-side writing of content
#[cfg_attr(feature = "clap", clap(value_parser = parse_byte_vec))]
data: ByteVec,
#[serde(with = "serde_bytes")]
#[cfg_attr(feature = "schemars", schemars(with = "Vec<u8>"))]
data: Vec<u8>,
},
/// Appends text to a file, creating it if it does not exist, on the remote machine
@ -231,7 +213,6 @@ pub enum DistantRequestData {
},
/// Reads a directory from the specified path on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["ls"]))]
#[strum_discriminants(strum(message = "Supports reading directory"))]
DirRead {
/// The path to the directory on the remote machine
@ -241,12 +222,10 @@ pub enum DistantRequestData {
/// depth and 1 indicating the most immediate children within the
/// directory
#[serde(default = "one")]
#[cfg_attr(feature = "clap", clap(long, default_value = "1"))]
depth: usize,
/// Whether or not to return absolute or relative paths
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
absolute: bool,
/// Whether or not to canonicalize the resulting paths, meaning
@ -256,7 +235,6 @@ pub enum DistantRequestData {
/// Note that the flag absolute must be true to have absolute paths
/// returned, even if canonicalize is flagged as true
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
canonicalize: bool,
/// Whether or not to include the root directory in the retrieved
@ -265,12 +243,10 @@ pub enum DistantRequestData {
/// If included, the root directory will also be a canonicalized,
/// absolute path and will not follow any of the other flags
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
include_root: bool,
},
/// Creates a directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["mkdir"]))]
#[strum_discriminants(strum(message = "Supports creating directory"))]
DirCreate {
/// The path to the directory on the remote machine
@ -278,12 +254,10 @@ pub enum DistantRequestData {
/// Whether or not to create all parent directories
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
all: bool,
},
/// Removes a file or directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["rm"]))]
#[strum_discriminants(strum(message = "Supports removing files, directories, and symlinks"))]
Remove {
/// The path to the file or directory on the remote machine
@ -292,12 +266,10 @@ pub enum DistantRequestData {
/// Whether or not to remove all contents within directory if is a directory.
/// Does nothing different for files
#[serde(default)]
#[cfg_attr(feature = "clap", clap(long))]
force: bool,
},
/// Copies a file or directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["cp"]))]
#[strum_discriminants(strum(message = "Supports copying files, directories, and symlinks"))]
Copy {
/// The path to the file or directory on the remote machine

@ -1,106 +0,0 @@
use crate::{data::Cmd, DistantMsg, DistantRequestData};
use clap::{
error::{Error, ErrorKind},
Arg, ArgAction, ArgMatches, Args, Command, FromArgMatches, Subcommand,
};
impl FromArgMatches for Cmd {
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
let mut matches = matches.clone();
Self::from_arg_matches_mut(&mut matches)
}
fn from_arg_matches_mut(matches: &mut ArgMatches) -> Result<Self, Error> {
let cmd = matches.get_one::<String>("cmd").ok_or_else(|| {
Error::raw(
ErrorKind::MissingRequiredArgument,
"program must be specified",
)
})?;
let args: Vec<String> = matches
.get_many::<String>("arg")
.unwrap_or_default()
.map(ToString::to_string)
.collect();
Ok(Self::new(format!("{cmd} {}", args.join(" "))))
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> {
let mut matches = matches.clone();
self.update_from_arg_matches_mut(&mut matches)
}
fn update_from_arg_matches_mut(&mut self, _matches: &mut ArgMatches) -> Result<(), Error> {
Ok(())
}
}
impl Args for Cmd {
fn augment_args(cmd: Command) -> Command {
cmd.arg(
Arg::new("cmd")
.required(true)
.value_name("CMD")
.action(ArgAction::Set),
)
.trailing_var_arg(true)
.arg(
Arg::new("arg")
.value_name("ARGS")
.num_args(1..)
.action(ArgAction::Append),
)
}
fn augment_args_for_update(cmd: Command) -> Command {
cmd
}
}
impl FromArgMatches for DistantMsg<DistantRequestData> {
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
match matches.subcommand() {
Some(("single", args)) => Ok(Self::Single(DistantRequestData::from_arg_matches(args)?)),
Some((_, _)) => Err(Error::raw(
ErrorKind::InvalidSubcommand,
"Valid subcommand is `single`",
)),
None => Err(Error::raw(
ErrorKind::MissingSubcommand,
"Valid subcommand is `single`",
)),
}
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> {
match matches.subcommand() {
Some(("single", args)) => {
*self = Self::Single(DistantRequestData::from_arg_matches(args)?)
}
Some((_, _)) => {
return Err(Error::raw(
ErrorKind::InvalidSubcommand,
"Valid subcommand is `single`",
))
}
None => (),
};
Ok(())
}
}
impl Subcommand for DistantMsg<DistantRequestData> {
fn augment_subcommands(cmd: Command) -> Command {
cmd.subcommand(DistantRequestData::augment_subcommands(Command::new(
"single",
)))
.subcommand_required(true)
}
fn augment_subcommands_for_update(cmd: Command) -> Command {
cmd.subcommand(DistantRequestData::augment_subcommands(Command::new(
"single",
)))
.subcommand_required(true)
}
fn has_subcommand(name: &str) -> bool {
matches!(name, "single")
}
}

@ -18,6 +18,13 @@ pub struct Error {
impl std::error::Error for Error {}
impl Error {
/// Produces an [`io::Error`] from this error.
pub fn to_io_error(&self) -> io::Error {
io::Error::new(self.kind.into(), self.description.to_string())
}
}
#[cfg(feature = "schemars")]
impl Error {
pub fn root_schema() -> schemars::schema::RootSchema {

@ -7,24 +7,19 @@ pub type SearchId = u32;
/// Represents a query to perform against the filesystem
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "clap", derive(clap::Args))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SearchQuery {
/// Kind of data to examine using condition
#[cfg_attr(feature = "clap", clap(long, value_enum, default_value_t = SearchQueryTarget::Contents))]
pub target: SearchQueryTarget,
/// Condition to meet to be considered a match
#[cfg_attr(feature = "clap", clap(name = "pattern"))]
pub condition: SearchQueryCondition,
/// Paths in which to perform the query
#[cfg_attr(feature = "clap", clap(default_value = "."))]
pub paths: Vec<PathBuf>,
/// Options to apply to the query
#[serde(default)]
#[cfg_attr(feature = "clap", clap(flatten))]
pub options: SearchQueryOptions,
}
@ -46,9 +41,7 @@ impl FromStr for SearchQuery {
/// Kind of data to examine using conditions
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "clap", clap(rename_all = "snake_case"))]
#[serde(rename_all = "snake_case")]
pub enum SearchQueryTarget {
/// Checks path of file, directory, or symlink
@ -176,32 +169,26 @@ impl FromStr for SearchQueryCondition {
/// Options associated with a search query
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "clap", derive(clap::Args))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SearchQueryOptions {
/// Restrict search to only these file types (otherwise all are allowed)
#[cfg_attr(feature = "clap", clap(skip))]
#[serde(default)]
pub allowed_file_types: HashSet<FileType>,
/// Regex to use to filter paths being searched to only those that match the include condition
#[cfg_attr(feature = "clap", clap(long))]
#[serde(default)]
pub include: Option<SearchQueryCondition>,
/// Regex to use to filter paths being searched to only those that do not match the exclude
/// condition
#[cfg_attr(feature = "clap", clap(long))]
#[serde(default)]
pub exclude: Option<SearchQueryCondition>,
/// Search should follow symbolic links
#[cfg_attr(feature = "clap", clap(long))]
#[serde(default)]
pub follow_symbolic_links: bool,
/// Maximum results to return before stopping the query
#[cfg_attr(feature = "clap", clap(long))]
#[serde(default)]
pub limit: Option<u64>,
@ -213,13 +200,11 @@ pub struct SearchQueryOptions {
///
/// Note that this will not simply filter the entries of the iterator, but it will actually
/// avoid descending into directories when the depth is exceeded.
#[cfg_attr(feature = "clap", clap(long))]
#[serde(default)]
pub max_depth: Option<u64>,
/// Amount of results to batch before sending back excluding final submission that will always
/// include the remaining results even if less than pagination request
#[cfg_attr(feature = "clap", clap(long))]
#[serde(default)]
pub pagination: Option<u64>,
}

@ -2,18 +2,19 @@ use crate::cli::common::{
Cache, Client, JsonAuthHandler, MsgReceiver, MsgSender, PromptAuthHandler,
};
use crate::constants::MAX_PIPE_CHUNK_SIZE;
use crate::options::{ClientSubcommand, Format, NetworkSettings};
use crate::options::{ClientFileSystemSubcommand, ClientSubcommand, Format, NetworkSettings};
use crate::{CliError, CliResult};
use anyhow::Context;
use distant_core::{
data::ChangeKindSet,
net::common::{ConnectionId, Host, Map, Request, Response},
net::manager::ManagerClient,
DistantMsg, DistantRequestData, DistantResponseData, RemoteCommand, Searcher, Watcher,
};
use distant_core::data::{FileType, SearchQuery, SystemInfo};
use distant_core::net::common::{ConnectionId, Host, Map, Request, Response};
use distant_core::net::manager::ManagerClient;
use distant_core::{DistantChannel, DistantChannelExt};
use distant_core::{DistantMsg, DistantRequestData, DistantResponseData, RemoteCommand, Searcher};
use log::*;
use serde_json::json;
use std::io::Write;
use std::{io, path::Path, time::Duration};
use tabled::{object::Rows, style::Style, Alignment, Disable, Modify, Table, Tabled};
use tokio::sync::mpsc;
mod lsp;
@ -39,164 +40,6 @@ async fn read_cache(path: &Path) -> Cache {
async fn async_run(cmd: ClientSubcommand) -> CliResult {
match cmd {
ClientSubcommand::Action {
cache,
connection,
network,
request,
timeout,
} => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
let timeout = match timeout {
Some(timeout) if timeout >= f32::EPSILON => Some(timeout),
_ => None,
};
debug!(
"Timeout configured to be {}",
match timeout {
Some(secs) => format!("{secs}s"),
None => "none".to_string(),
}
);
let mut formatter = Formatter::shell();
debug!("Sending request {:?}", request);
match request {
DistantRequestData::ProcSpawn {
cmd,
environment,
current_dir,
pty,
} => {
debug!("Special request spawning {:?}", cmd);
let mut proc = RemoteCommand::new()
.environment(environment)
.current_dir(current_dir)
.pty(pty)
.spawn(channel.into_client().into_channel(), cmd.as_str())
.await
.with_context(|| format!("Failed to spawn {cmd}"))?;
// Now, map the remote process' stdin/stdout/stderr to our own process
let link = RemoteProcessLink::from_remote_pipes(
proc.stdin.take(),
proc.stdout.take().unwrap(),
proc.stderr.take().unwrap(),
MAX_PIPE_CHUNK_SIZE,
);
let status = proc.wait().await.context("Failed to wait for process")?;
// Shut down our link
link.shutdown().await;
if !status.success {
if let Some(code) = status.code {
return Err(CliError::Exit(code as u8));
} else {
return Err(CliError::FAILURE);
}
}
}
DistantRequestData::Search { query } => {
debug!("Special request creating searcher for {:?}", query);
let mut searcher =
Searcher::search(channel.into_client().into_channel(), query)
.await
.context("Failed to start search")?;
// Continue to receive and process matches
while let Some(m) = searcher.next().await {
// TODO: Provide a cleaner way to print just a match
let res = Response::new(
"".to_string(),
DistantMsg::Single(DistantResponseData::SearchResults {
id: 0,
matches: vec![m],
}),
);
formatter.print(res).context("Failed to print match")?;
}
}
DistantRequestData::Watch {
path,
recursive,
only,
except,
} => {
debug!("Special request creating watcher for {:?}", path);
let mut watcher = Watcher::watch(
channel.into_client().into_channel(),
path.as_path(),
recursive,
only.into_iter().collect::<ChangeKindSet>(),
except.into_iter().collect::<ChangeKindSet>(),
)
.await
.with_context(|| format!("Failed to watch {path:?}"))?;
// Continue to receive and process changes
while let Some(change) = watcher.next().await {
// TODO: Provide a cleaner way to print just a change
let res = Response::new(
"".to_string(),
DistantMsg::Single(DistantResponseData::Changed(change)),
);
formatter.print(res).context("Failed to print change")?;
}
}
request => {
let response = channel
.into_client()
.into_channel()
.send_timeout(
DistantMsg::Single(request),
timeout.map(Duration::from_secs_f32),
)
.await
.context("Failed to send request")?;
debug!("Got response {:?}", response);
// NOTE: We expect a single response, and if that is an error then
// we want to pass that error up the stack
let id = response.id;
let origin_id = response.origin_id;
match response.payload {
DistantMsg::Single(DistantResponseData::Error(x)) => {
return Err(CliError::Error(anyhow::anyhow!(x)));
}
payload => formatter
.print(Response {
id,
origin_id,
payload,
})
.context("Failed to print response")?,
}
}
}
}
ClientSubcommand::Connect {
cache,
destination,
@ -334,36 +177,6 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
),
}
}
ClientSubcommand::Lsp {
cache,
cmd,
connection,
current_dir,
network,
pty,
} => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
debug!("Spawning LSP server (pty = {}): {}", pty, cmd);
Lsp::new(channel.into_client().into_channel())
.spawn(cmd, current_dir, pty, MAX_PIPE_CHUNK_SIZE)
.await?;
}
ClientSubcommand::Repl {
cache,
connection,
@ -503,8 +316,8 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
}
ClientSubcommand::Shell {
cache,
cmd,
connection,
cmd,
current_dir,
environment,
network,
@ -526,6 +339,9 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
// Convert cmd into string
let cmd = cmd.map(Into::into);
debug!(
"Spawning shell (environment = {:?}): {}",
environment,
@ -535,6 +351,498 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
.spawn(cmd, environment, current_dir, MAX_PIPE_CHUNK_SIZE)
.await?;
}
ClientSubcommand::Spawn {
cache,
connection,
cmd,
current_dir,
environment,
lsp,
pty,
network,
} => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
if lsp {
debug!(
"Spawning LSP server (pty = {}, cwd = {:?}): {}",
pty, current_dir, cmd
);
Lsp::new(channel.into_client().into_channel())
.spawn(cmd, current_dir, pty, MAX_PIPE_CHUNK_SIZE)
.await?;
} else if pty {
// Convert cmd into string
let cmd = String::from(cmd);
debug!(
"Spawning pty process (environment = {:?}, cwd = {:?}): {}",
environment, current_dir, cmd
);
Shell::new(channel.into_client().into_channel())
.spawn(cmd, environment, current_dir, MAX_PIPE_CHUNK_SIZE)
.await?;
} else {
debug!(
"Spawning regular process (environment = {:?}, cwd = {:?}): {}",
environment, current_dir, cmd
);
let mut proc = RemoteCommand::new()
.environment(environment)
.current_dir(current_dir)
.pty(None)
.spawn(channel.into_client().into_channel(), cmd.as_str())
.await
.with_context(|| format!("Failed to spawn {cmd}"))?;
// Now, map the remote process' stdin/stdout/stderr to our own process
let link = RemoteProcessLink::from_remote_pipes(
proc.stdin.take(),
proc.stdout.take().unwrap(),
proc.stderr.take().unwrap(),
MAX_PIPE_CHUNK_SIZE,
);
let status = proc.wait().await.context("Failed to wait for process")?;
// Shut down our link
link.shutdown().await;
if !status.success {
if let Some(code) = status.code {
return Err(CliError::Exit(code as u8));
} else {
return Err(CliError::FAILURE);
}
}
}
}
ClientSubcommand::SystemInfo {
cache,
connection,
network,
} => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
debug!("Retrieving system information");
let SystemInfo {
family,
os,
arch,
current_dir,
main_separator,
username,
shell,
} = channel
.into_client()
.into_channel()
.system_info()
.await
.with_context(|| {
format!(
"Failed to retrieve system information using connection {connection_id}"
)
})?;
let mut out = std::io::stdout();
out.write_all(
&format!(
concat!(
"Family: {:?}\n",
"Operating System: {:?}\n",
"Arch: {:?}\n",
"Cwd: {:?}\n",
"Path Sep: {:?}\n",
"Username: {:?}\n",
"Shell: {:?}"
),
family, os, arch, current_dir, main_separator, username, shell
)
.into_bytes(),
)
.context("Failed to write system information to stdout")?;
out.flush().context("Failed to flush stdout")?;
}
ClientSubcommand::FileSystem(ClientFileSystemSubcommand::Copy {
cache,
connection,
network,
src,
dst,
}) => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
debug!("Copying {src:?} to {dst:?}");
channel
.into_client()
.into_channel()
.copy(src.as_path(), dst.as_path())
.await
.with_context(|| {
format!("Failed to copy {src:?} to {dst:?} using connection {connection_id}")
})?;
}
ClientSubcommand::FileSystem(ClientFileSystemSubcommand::MakeDir {
cache,
connection,
network,
path,
all,
}) => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
debug!("Making directory {path:?} (all = {all})");
channel
.into_client()
.into_channel()
.create_dir(path.as_path(), all)
.await
.with_context(|| {
format!("Failed to make directory {path:?} using connection {connection_id}")
})?;
}
ClientSubcommand::FileSystem(ClientFileSystemSubcommand::Read {
cache,
connection,
network,
path,
depth,
absolute,
canonicalize,
include_root,
}) => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let mut channel: DistantChannel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?
.into_client()
.into_channel();
// NOTE: We don't know whether the path is for a file or directory, so we try both
// at the same time and return the first result, or fail if both fail!
debug!(
"Reading {path:?} (depth = {}, absolute = {}, canonicalize = {}, include_root = {})",
depth, absolute, canonicalize, include_root
);
let results = channel
.send(DistantMsg::Batch(vec![
DistantRequestData::DirRead {
path: path.to_path_buf(),
depth,
absolute,
canonicalize,
include_root,
},
DistantRequestData::FileRead {
path: path.to_path_buf(),
},
]))
.await
.with_context(|| {
format!("Failed to read {path:?} using connection {connection_id}")
})?;
let mut errors = Vec::new();
for response in results
.payload
.into_batch()
.context("Got single response to batch request")?
{
match response {
DistantResponseData::DirEntries { entries, .. } => {
#[derive(Tabled)]
struct EntryRow {
ty: String,
path: String,
}
let data = Table::new(entries.into_iter().map(|entry| EntryRow {
ty: String::from(match entry.file_type {
FileType::Dir => "<DIR>",
FileType::File => "",
FileType::Symlink => "<SYMLINK>",
}),
path: entry.path.to_string_lossy().to_string(),
}))
.with(Style::blank())
.with(Disable::row(Rows::new(..1)))
.with(Modify::new(Rows::new(..)).with(Alignment::left()))
.to_string()
.into_bytes();
let mut out = std::io::stdout();
out.write_all(&data)
.context("Failed to write directory contents to stdout")?;
out.flush().context("Failed to flush stdout")?;
}
DistantResponseData::Blob { data } => {
let mut out = std::io::stdout();
out.write_all(&data)
.context("Failed to write file contents to stdout")?;
out.flush().context("Failed to flush stdout")?;
}
DistantResponseData::Error(x) => errors.push(x),
_ => continue,
}
}
if let Some(x) = errors.first() {
return Err(CliError::from(anyhow::anyhow!(x.to_io_error())));
}
}
ClientSubcommand::FileSystem(ClientFileSystemSubcommand::Remove {
cache,
connection,
network,
path,
force,
}) => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
debug!("Removing {path:?} (force = {force}");
channel
.into_client()
.into_channel()
.remove(path.as_path(), force)
.await
.with_context(|| {
format!("Failed to remove {path:?} using connection {connection_id}")
})?;
}
ClientSubcommand::FileSystem(ClientFileSystemSubcommand::Rename {
cache,
connection,
network,
src,
dst,
}) => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
debug!("Renaming {src:?} to {dst:?}");
channel
.into_client()
.into_channel()
.rename(src.as_path(), dst.as_path())
.await
.with_context(|| {
format!("Failed to rename {src:?} to {dst:?} using connection {connection_id}")
})?;
}
ClientSubcommand::FileSystem(ClientFileSystemSubcommand::Search {
cache,
connection,
network,
target,
condition,
options,
paths,
}) => {
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
let mut formatter = Formatter::shell();
let query = SearchQuery {
target: target.into(),
condition,
paths,
options: options.into(),
};
let mut searcher = Searcher::search(channel.into_client().into_channel(), query)
.await
.context("Failed to start search")?;
// Continue to receive and process matches
while let Some(m) = searcher.next().await {
// TODO: Provide a cleaner way to print just a match
let res = Response::new(
"".to_string(),
DistantMsg::Single(DistantResponseData::SearchResults {
id: 0,
matches: vec![m],
}),
);
formatter.print(res).context("Failed to print match")?;
}
}
ClientSubcommand::FileSystem(ClientFileSystemSubcommand::Write {
cache,
connection,
network,
append,
path,
data,
}) => {
let data = match data {
Some(x) => x,
None => {
debug!("No data provided, reading from stdin");
use std::io::Read;
let mut buf = Vec::new();
std::io::stdin()
.read_to_end(&mut buf)
.context("Failed to read stdin")?;
buf
}
};
debug!("Connecting to manager");
let mut client = Client::new(network)
.using_prompt_auth_handler()
.connect()
.await
.context("Failed to connect to manager")?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
if append {
debug!("Appending contents to {path:?}");
channel
.into_client()
.into_channel()
.append_file(path.as_path(), data)
.await
.with_context(|| {
format!("Failed to write to {path:?} using connection {connection_id}")
})?;
} else {
debug!("Writing contents to {path:?}");
channel
.into_client()
.into_channel()
.write_file(path.as_path(), data)
.await
.with_context(|| {
format!("Failed to write to {path:?} using connection {connection_id}")
})?;
}
}
}
Ok(())

File diff suppressed because it is too large Load Diff

@ -1,9 +1,13 @@
mod address;
mod cmd;
mod logging;
mod network;
mod search;
mod value;
pub use address::*;
pub use cmd::*;
pub use logging::*;
pub use network::*;
pub use search::*;
pub use value::*;

@ -0,0 +1,101 @@
use clap::error::{Error, ErrorKind};
use clap::{Arg, ArgAction, ArgMatches, Args, Command, FromArgMatches};
use derive_more::{Display, From, Into};
use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};
/// Represents some command with arguments to execute
#[derive(Clone, Debug, Display, From, Into, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cmd(String);
impl Cmd {
/// Creates a new command from the given `cmd`
pub fn new(cmd: impl Into<String>) -> Self {
Self(cmd.into())
}
/// Returns reference to the program portion of the command
pub fn program(&self) -> &str {
match self.0.split_once(' ') {
Some((program, _)) => program.trim(),
None => self.0.trim(),
}
}
/// Returns reference to the arguments portion of the command
pub fn arguments(&self) -> &str {
match self.0.split_once(' ') {
Some((_, arguments)) => arguments.trim(),
None => "",
}
}
}
impl Deref for Cmd {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Cmd {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> From<&'a str> for Cmd {
fn from(s: &'a str) -> Self {
Self(s.to_string())
}
}
impl FromArgMatches for Cmd {
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
let mut matches = matches.clone();
Self::from_arg_matches_mut(&mut matches)
}
fn from_arg_matches_mut(matches: &mut ArgMatches) -> Result<Self, Error> {
let cmd = matches.get_one::<String>("cmd").ok_or_else(|| {
Error::raw(
ErrorKind::MissingRequiredArgument,
"program must be specified",
)
})?;
let args: Vec<String> = matches
.get_many::<String>("arg")
.unwrap_or_default()
.map(ToString::to_string)
.collect();
Ok(Self::new(format!("{cmd} {}", args.join(" "))))
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> {
let mut matches = matches.clone();
self.update_from_arg_matches_mut(&mut matches)
}
fn update_from_arg_matches_mut(&mut self, _matches: &mut ArgMatches) -> Result<(), Error> {
Ok(())
}
}
impl Args for Cmd {
fn augment_args(cmd: Command) -> Command {
cmd.arg(
Arg::new("cmd")
.required(true)
.value_name("CMD")
.action(ArgAction::Set),
)
.trailing_var_arg(true)
.arg(
Arg::new("arg")
.value_name("ARGS")
.num_args(1..)
.action(ArgAction::Append),
)
}
fn augment_args_for_update(cmd: Command) -> Command {
cmd
}
}

@ -0,0 +1,81 @@
use clap::{Args, ValueEnum};
use distant_core::data::FileType;
use distant_core::data::{SearchQueryOptions, SearchQueryTarget};
use std::collections::HashSet;
pub use distant_core::data::SearchQueryCondition as CliSearchQueryCondition;
/// Options to customize the search results.
#[derive(Args, Clone, Debug, Default, PartialEq, Eq)]
pub struct CliSearchQueryOptions {
/// Restrict search to only these file types (otherwise all are allowed)
#[clap(skip)]
pub allowed_file_types: HashSet<FileType>,
/// Regex to use to filter paths being searched to only those that match the include condition
#[clap(long)]
pub include: Option<CliSearchQueryCondition>,
/// Regex to use to filter paths being searched to only those that do not match the exclude
/// condition
#[clap(long)]
pub exclude: Option<CliSearchQueryCondition>,
/// Search should follow symbolic links
#[clap(long)]
pub follow_symbolic_links: bool,
/// Maximum results to return before stopping the query
#[clap(long)]
pub limit: Option<u64>,
/// Maximum depth (directories) to search
///
/// The smallest depth is 0 and always corresponds to the path given to the new function on
/// this type. Its direct descendents have depth 1, and their descendents have depth 2, and so
/// on.
///
/// Note that this will not simply filter the entries of the iterator, but it will actually
/// avoid descending into directories when the depth is exceeded.
#[clap(long)]
pub max_depth: Option<u64>,
/// Amount of results to batch before sending back excluding final submission that will always
/// include the remaining results even if less than pagination request
#[clap(long)]
pub pagination: Option<u64>,
}
impl From<CliSearchQueryOptions> for SearchQueryOptions {
fn from(x: CliSearchQueryOptions) -> Self {
Self {
allowed_file_types: x.allowed_file_types,
include: x.include,
exclude: x.exclude,
follow_symbolic_links: x.follow_symbolic_links,
limit: x.limit,
max_depth: x.max_depth,
pagination: x.pagination,
}
}
}
/// Kind of data to examine using conditions
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "snake_case")]
pub enum CliSearchQueryTarget {
/// Checks path of file, directory, or symlink
Path,
/// Checks contents of files
Contents,
}
impl From<CliSearchQueryTarget> for SearchQueryTarget {
fn from(x: CliSearchQueryTarget) -> Self {
match x {
CliSearchQueryTarget::Contents => Self::Contents,
CliSearchQueryTarget::Path => Self::Path,
}
}
}

@ -108,7 +108,6 @@ mod tests {
config,
Config {
client: ClientConfig {
action: ClientActionConfig { timeout: Some(0.) },
connect: ClientConnectConfig {
options: Map::new()
},
@ -177,9 +176,6 @@ log_level = "trace"
unix_socket = "client-unix-socket"
windows_pipe = "client-windows-pipe"
[client.action]
timeout = 123
[client.connect]
options = "key=\"value\",key2=\"value2\""
@ -222,9 +218,6 @@ current_dir = "server-current-dir"
config,
Config {
client: ClientConfig {
action: ClientActionConfig {
timeout: Some(123.)
},
connect: ClientConnectConfig {
options: map!("key" -> "value", "key2" -> "value2"),
},

@ -22,13 +22,6 @@ log_level = "info"
# manager (Windows only)
# windows_pipe = "some_name"
# Configuration related to the client's action command
[client.action]
# Maximum time (in seconds) to wait for a network request before timing out
# where 0 indicates no timeout will occur
timeout = 0
# Configuration related to the client's connect command
[client.connect]

@ -1,12 +1,10 @@
use super::common::{self, LoggingSettings, NetworkSettings};
use serde::{Deserialize, Serialize};
mod action;
mod connect;
mod launch;
mod repl;
pub use action::*;
pub use connect::*;
pub use launch::*;
pub use repl::*;
@ -20,7 +18,6 @@ pub struct ClientConfig {
#[serde(flatten)]
pub network: NetworkSettings,
pub action: ClientActionConfig,
pub connect: ClientConnectConfig,
pub launch: ClientLaunchConfig,
pub repl: ClientReplConfig,

@ -1,6 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ClientActionConfig {
pub timeout: Option<f32>,
}
Loading…
Cancel
Save