Skip to content

Commit

Permalink
update install functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
chanderlud committed Jan 23, 2024
1 parent 84a1c76 commit 5adb4e5
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 23 deletions.
27 changes: 26 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub(crate) enum ErrorKind {
Octocrab(octocrab::Error),
#[cfg(feature = "installer")]
Sftp(russh_sftp::client::error::Error),
#[cfg(feature = "installer")]
SemVer(semver::Error),
MissingQueue,
MaxRetries,
#[cfg(windows)]
Expand All @@ -56,9 +58,12 @@ pub(crate) enum ErrorKind {
/// no suitable release was found on github
#[cfg(feature = "installer")]
NoSuitableRelease,
/// the file was not found in the archive
/// the file was not found
#[cfg(feature = "installer")]
FileNotFound,
/// the directory was not found
#[cfg(feature = "installer")]
DirectoryNotFound,
}

impl From<io::Error> for Error {
Expand Down Expand Up @@ -193,6 +198,15 @@ impl From<russh_sftp::client::error::Error> for Error {
}
}

#[cfg(feature = "installer")]
impl From<semver::Error> for Error {
fn from(error: semver::Error) -> Self {
Self {
kind: ErrorKind::SemVer(error),
}
}
}

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.kind {
Expand All @@ -216,6 +230,8 @@ impl std::fmt::Display for Error {
ErrorKind::Octocrab(ref error) => write!(f, "octocrab error: {}", error),
#[cfg(feature = "installer")]
ErrorKind::Sftp(ref error) => write!(f, "sftp error: {}", error),
#[cfg(feature = "installer")]
ErrorKind::SemVer(ref error) => write!(f, "semver error: {}", error),
ErrorKind::MissingQueue => write!(f, "missing queue"),
ErrorKind::MaxRetries => write!(f, "max retries"),
#[cfg(windows)]
Expand All @@ -241,6 +257,8 @@ impl std::fmt::Display for Error {
}
#[cfg(feature = "installer")]
ErrorKind::FileNotFound => write!(f, "file not found"),
#[cfg(feature = "installer")]
ErrorKind::DirectoryNotFound => write!(f, "directory not found"),
}
}
}
Expand Down Expand Up @@ -321,6 +339,13 @@ impl Error {
}
}

#[cfg(feature = "installer")]
pub(crate) fn directory_not_found() -> Self {
Self {
kind: ErrorKind::DirectoryNotFound,
}
}

#[cfg(windows)]
pub(crate) fn status_error() -> Self {
Self {
Expand Down
74 changes: 59 additions & 15 deletions src/install/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use russh_sftp::client::SftpSession;
use semver::Version;
use tar::Archive;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tokio::io::{stdin, stdout, AsyncBufReadExt, AsyncWriteExt, BufReader};

use crate::error::Error;
use crate::error::{Error, ErrorKind};
use crate::Result;

const WINDOWS_TARGET: &str = include_str!("target.bat");
Expand All @@ -27,17 +27,52 @@ pub(crate) async fn installer(
client: Client,
mut destination: PathBuf,
custom_binary: Option<PathBuf>,
mut overwrite: bool,
) -> Result<()> {
let os = identify_os(&client).await?; // identify the remote os

if is_dir(&client, &os, &destination).await? {
// append the binary name to the destination if it is a directory
match os {
Os::Windows => destination.push("cccp.exe"),
Os::UnixLike => destination.push("cccp"),
// loop until the destination is a file
loop {
match is_dir(&client, &os, &destination).await {
Ok(true) => {
// append the binary name to the destination if it is a directory
match os {
Os::Windows => destination.push("cccp.exe"),
Os::UnixLike => destination.push("cccp"),
}

debug!("appended binary name to destination: {:?}", destination);
}
Ok(false) => {
if !overwrite {
// async stdout prompt
let mut stdout = stdout();
stdout
.write_all(b"file already exists, overwrite? [y/N] ")
.await?;
stdout.flush().await?;

let mut input = String::new(); // buffer for input
let mut stdin = BufReader::new(stdin()); // stdin reader

stdin.read_line(&mut input).await?; // read input into buffer
input.make_ascii_lowercase(); // convert to lowercase

overwrite = input.starts_with('y'); // update overwrite
}

if overwrite {
break;
} else {
return Ok(());
}
}
Err(error) => match error.kind {
// if the file does not exist, that is fine
ErrorKind::FileNotFound => break,
_ => return Err(error),
},
}

debug!("appended binary name to destination: {:?}", destination);
}

if let Some(file_path) = custom_binary {
Expand Down Expand Up @@ -66,10 +101,11 @@ pub(crate) async fn installer(

info!("installing cccp-{}-{}", release.tag_name, triple);

// cccp tags are always prefixed with a v
let tag_version = release.tag_name.strip_prefix('v').unwrap();

// check if the local version is out of date
if Version::parse(VERSION).unwrap()
< Version::parse(release.tag_name.strip_prefix('v').unwrap()).unwrap()
{
if Version::parse(VERSION)? < Version::parse(tag_version)? {
warn!(
"the local version of cccp is out of date [v{} < {}]",
VERSION, release.tag_name
Expand Down Expand Up @@ -157,11 +193,18 @@ async fn transfer_binary(client: &Client, content: &[u8], destination: PathBuf)
}

async fn is_dir(client: &Client, os: &Os, path: &Path) -> Result<bool> {
let parent_directory = path.parent().map_or(".".into(), |p| p.to_string_lossy());
let path = path.to_string_lossy();

let command = match os {
Os::Windows => format!("if exist \"{}\" ( if not exist \"{}\" ( echo false ) else ( echo true )) else (echo error)", path, path),
Os::UnixLike => format!("[ -d \"{}\" ] && echo true || ([ -f \"{}\" ] && echo false || echo error)", path, path),
Os::Windows => format!(
"if exist \"{}\" ( if exist \"{}\" ( echo true ) else ( if exist \"{}\" ( echo false ) else ( echo not_found ))) else (echo dnf)",
parent_directory, path, path
),
Os::UnixLike => format!(
"[ -d \"{}\" ] && ( [ -d \"{}\" ] && echo true || ([ -f \"{}\" ] && echo false || echo not_found )) || echo dnf",
parent_directory, path, path
),
};

let result = client.execute(&command).await?;
Expand All @@ -170,7 +213,8 @@ async fn is_dir(client: &Client, os: &Os, path: &Path) -> Result<bool> {
match result.stdout.chars().next() {
Some('t') => Ok(true),
Some('f') => Ok(false),
Some('e') => Err(Error::file_not_found()),
Some('n') => Err(Error::file_not_found()),
Some('d') => Err(Error::directory_not_found()),
_ => Err(Error::command_failed(result.exit_status)),
}
} else {
Expand Down
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,13 @@ async fn install(options: InstallOptions) -> Result<()> {

if let Some(host) = options.destination.host {
let client = connect_client(host, &options.destination.username).await?;
install::installer(client, options.destination.file_path, options.custom_binary).await
install::installer(
client,
options.destination.file_path,
options.custom_binary,
options.overwrite,
)
.await
} else {
let mut command = InstallOptions::command();

Expand Down
19 changes: 13 additions & 6 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ const HELP_HEADING: &str = "\x1B[1m\x1B[4mAbout\x1B[0m
- The first two ports are used for TCP streams which carry control messages
- The remaining ports are UDP sockets which carry data";

#[cfg(feature = "installer")]
const INSTALL_HEADING: &str = "\x1B[1m\x1B[4mInstallation Tips\x1B[0m
- The installation location should be in PATH. Alternatively, pass the absolute path to the `command` transfer argument
- The filename should be included in the destionation IoSpec (/usr/bin/\x1B[4m\x1B[1mcccp\x1B[0m)
- /bin/cccp or /usr/bin/cccp are be good choices on Unix";
- The installation location should be in PATH. Alternatively, pass the absolute path to the \x1B[1m\x1B[4mcommand\x1B[0m transfer argument
- /usr/bin/cccp is a good choice on Linux and BSD
- /usr/local/bin/cccp is a good choice on macOS
- On Windows you could install to C:\\Windows or C:\\Windows\\System32 but this is not recommended";

#[derive(Parser)]
#[clap(version, about = HELP_HEADING)]
Expand Down Expand Up @@ -194,19 +196,24 @@ impl Options {
}
}

#[cfg(feature = "installer")]
#[derive(Parser)]
#[clap(version, about = INSTALL_HEADING)]
pub(crate) struct InstallOptions {
/// IoSpec for the remote host & install location
pub(crate) destination: IoSpec,

/// Log level [debug, info, warn, error]
#[clap(short, long, default_value = "warn")]
pub(crate) log_level: LevelFilter,

/// Use a custom binary instead of downloading from Github
#[clap(short, long)]
pub(crate) custom_binary: Option<PathBuf>,

/// Overwrite an existing file without prompting
#[clap(short, long)]
pub(crate) overwrite: bool,

/// IoSpec for the remote host & install location
pub(crate) destination: IoSpec,
}

#[derive(Clone, PartialEq)]
Expand Down

0 comments on commit 5adb4e5

Please sign in to comment.