use std::path::PathBuf;
use clap::{value_parser, Arg, ArgMatches, Command};
use validator::Validate;
use validator::ValidationErrors;
use crate::errors::SftpManError;
use crate::model::{FilesystemMountDefinition, DEFAULT_MOUNT_PATH_PREFIX};
use crate::utils::validation::errors_to_string_list;
use crate::AuthType;
use crate::Manager;
use super::exit;
const ARG_ID: &str = "id";
const ARG_HOST: &str = "host";
const ARG_PORT: &str = "port";
const ARG_USER: &str = "user";
const ARG_REMOTE_PATH: &str = "remote_path";
const ARG_MOUNT_OPT: &str = "mount_opt";
const ARG_MOUNT_PATH: &str = "mount_path";
const ARG_AUTH_TYPE: &str = "auth_type";
const ARG_SSH_KEY: &str = "ssh_key";
const ARG_CMD_BEFORE_MOUNT: &str = "cmd_before_mount";
pub fn build_create() -> Command {
Command::new("create")
.about("Creates a new filesystem mount definition")
.arg(
Arg::new(ARG_ID)
.long(ARG_ID)
.num_args(1)
.required(true)
.help("Unique identifier. Example: my-machine")
)
.arg(
Arg::new(ARG_HOST)
.long(ARG_HOST)
.num_args(1)
.required(true)
.help("Hostname or IP address")
)
.arg(
Arg::new(ARG_PORT)
.long(ARG_PORT)
.num_args(1)
.value_parser(value_parser!(u16).range(0..65535))
.default_value("22")
.required(false)
.help("SSH port number")
)
.arg(
Arg::new(ARG_USER)
.long(ARG_USER)
.num_args(1)
.required(true)
.help("Username to authenticate with")
)
.arg(
Arg::new(ARG_MOUNT_OPT)
.long(ARG_MOUNT_OPT)
.num_args(1)
.help("Options to pass to sshfs (via -o), separated by comma. Example: follow_symlinks,workaround=rename")
)
.arg(
Arg::new(ARG_REMOTE_PATH)
.long(ARG_REMOTE_PATH)
.required(true)
.help("Path on the remote machine that will be mounted locally. Example: /srv/http")
)
.arg(
Arg::new(ARG_MOUNT_PATH)
.long(ARG_MOUNT_PATH)
.help(format!(
"Path on the current machine where the remote path would be mounted. Example: /home/user/Desktop/http. Default: {0}/my-machine",
DEFAULT_MOUNT_PATH_PREFIX
))
)
.arg(
Arg::new(ARG_AUTH_TYPE)
.long(ARG_AUTH_TYPE)
.required(true)
.value_parser(clap::builder::EnumValueParser::<AuthType>::new())
.help("SSH authentication type")
)
.arg(
Arg::new(ARG_SSH_KEY)
.long(ARG_SSH_KEY)
.required_if_eq(ARG_AUTH_TYPE, AuthType::PublicKey.to_static_str())
.value_parser(clap::builder::PathBufValueParser::new())
.help(format!(
"SSH private key path. Only applies when --auth_type={0}. Example: /home/user/.ssh/id_ed25519",
AuthType::PublicKey.to_static_str(),
))
)
.arg(
Arg::new(ARG_CMD_BEFORE_MOUNT)
.long(ARG_CMD_BEFORE_MOUNT)
.required(false)
.help("Custom command to run every time before mounting. Example: /bin/true")
)
}
pub fn run_create(manager: &Manager, matches: &ArgMatches) -> exit::Status {
let id = matches.get_one::<String>(ARG_ID).expect("required");
match manager.definition(id) {
Ok(_) => {
log::error!("There already is a definition with an id of: {0}.", id);
log::error!("Consider updating it or removing & creating it anew.");
exit::Status::DefinitionAlreadyExists
}
Err(err) => match err {
SftpManError::JSON(_path, _serde_error) => {
log::error!("There already is a definition with an id of: {0}, but its data cannot be parsed", id);
exit::Status::Failure
}
_ => create(manager, id, matches),
},
}
}
fn create(manager: &Manager, id: &str, matches: &ArgMatches) -> exit::Status {
let mut definition = FilesystemMountDefinition {
id: id.to_owned(),
..Default::default()
};
bind_command_arguments_to_definition(matches, &mut definition, true);
if let Err(errors) = definition.validate() {
return abort_with_validation_errors(errors);
}
if let Err(err) = manager.persist(&definition) {
log::error!("Failed to persist definition: {0}", err);
return exit::Status::Failure;
}
exit::Status::Success
}
pub fn build_update() -> Command {
let mut cmd = Command::new("update").about("Updates an existing filesystem mount definition");
for arg_ref in build_create().get_arguments() {
let mut arg = arg_ref.to_owned();
if *arg.get_id() != ARG_ID {
arg = arg.required(false);
}
if *arg.get_id() == ARG_SSH_KEY {
let value_parser = arg.get_value_parser();
let mut arg_ssh_key = Arg::new(arg.get_id())
.long(ARG_SSH_KEY)
.value_parser(value_parser.to_owned());
if let Some(help) = arg.get_help() {
arg_ssh_key = arg_ssh_key.help(help.to_string());
}
arg = arg_ssh_key
}
cmd = cmd.arg(arg);
}
cmd
}
pub fn run_update(manager: &Manager, matches: &ArgMatches) -> exit::Status {
let id = matches.get_one::<String>(ARG_ID).expect("required");
match manager.definition(id) {
Ok(mut definition) => update(manager, &mut definition, matches),
Err(err) => {
log::error!("Failed to find or load definition: {0}: {1}", id, err);
exit::Status::DefinitionNotFound
}
}
}
fn update(
manager: &Manager,
definition: &mut FilesystemMountDefinition,
matches: &ArgMatches,
) -> exit::Status {
bind_command_arguments_to_definition(matches, definition, false);
if let Err(errors) = definition.validate() {
return abort_with_validation_errors(errors);
}
if let Err(err) = manager.persist(definition) {
log::error!("Failed to persist definition: {0}", err);
return exit::Status::Failure;
}
exit::Status::Success
}
fn bind_command_arguments_to_definition(
matches: &ArgMatches,
definition: &mut FilesystemMountDefinition,
is_new: bool,
) {
if let Some(value) = matches.get_one::<String>(ARG_HOST) {
definition.host = value.clone().to_owned();
}
if let Some(value) = matches.get_one::<u16>(ARG_PORT) {
definition.port = *value;
}
if let Some(value) = matches.get_one::<String>(ARG_USER) {
definition.user = value.clone().to_owned();
}
if let Some(value) = matches.get_one::<String>(ARG_MOUNT_OPT) {
definition.mount_options.clear();
for v in value.split(',') {
if !v.is_empty() {
definition.mount_options.push(v.to_owned());
}
}
}
if let Some(value) = matches.get_one::<String>(ARG_REMOTE_PATH) {
definition.remote_path = value.clone().to_owned();
}
if let Some(value) = matches.get_one::<String>(ARG_MOUNT_PATH) {
if value.is_empty() {
definition.mount_dest_path = None;
} else {
definition.mount_dest_path = Some(value.clone().to_owned());
}
}
if let Some(value) = matches.get_one::<String>(ARG_CMD_BEFORE_MOUNT) {
definition.cmd_before_mount = value.clone().to_owned();
}
if let Some(value) = matches.get_one::<AuthType>(ARG_AUTH_TYPE) {
definition.auth_type = value.clone().to_owned();
}
if !is_new && definition.auth_type != AuthType::PublicKey {
definition.ssh_key = "".to_owned();
}
if let Some(value) = matches.get_one::<PathBuf>(ARG_SSH_KEY) {
definition.ssh_key = value.to_string_lossy().into();
}
}
fn abort_with_validation_errors(errors: ValidationErrors) -> exit::Status {
log::error!("Validation failed with the following errors:");
for err in errors_to_string_list(errors) {
log::error!("- {0}", err);
}
exit::Status::ValidationFailure
}