From 84a1c76bd82ff1bce406bcf35e1dcf3067651591 Mon Sep 17 00:00:00 2001 From: chanderlud Date: Mon, 22 Jan 2024 16:57:06 -0800 Subject: [PATCH 1/2] rollback to dpl v1 --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 978fa8c..af49c3f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -107,7 +107,6 @@ after_script: set +e before_deploy: bash ci/before_deploy.sh deploy: - edge: true token: secure: gCv8cVWxx3HL4lFw0gwC5vIl4tyyPTG2ulquxZbrzRBeob6Ys39Rj9KBYis4bY44jXFKNBMbmyCKPGB6l5hoqTCKk2T5evOauWrZm1m2nzzRgeZ5OSKOpFMevO8rJA7Kc52lOvrdBtmhpnNjo9J5FDrc6N28u3TBhZ3DUqhCFMSs6GfXBCLbK7pIGcZeeNSujfoRx9tiF2AdXcbD9Pn3jSXRozvcItjqfjMEyBFqyAhVz4dDvEb/TiCdqyffs7Xr1Er886xEIdDv4i5g6T+SAQDj2+2/8W4nysjArIYMxIRcqDBqaHd07tHPg5AWZDq4Wg9knpzA5Hu7UXv00iSejPsbzElxwHa3oyTF0yIGyldt4qe7/ru593TTOZXS8Cwo7EsYdfUWrfhsl/4otGet/Ta/QaBeHwf6Sx1e6IPstbIwzvfI0jG2VE8yBO4Afb/BkS2Ufwawxt1KoXOP8ES+I+UR05M4shGBvNvZLqCftH7TUPlgpRmZGVz5OOgPi7UFOm19Qf3IPDS/8GlDN+T1wzNQbVTrU+qP6ah0mPEXOwrJeLF2fNDiQzVFgl/mg60osFHHmLwKcAxPzkkZjtCbIt830PRgg43W8yTDERnorvw/IdfLOvu0ngseFER8p8iN16M3IvDcKJ3HKGkURN5gBAqdtLA56HZdAj21uD+waXw= file_glob: true @@ -115,7 +114,7 @@ deploy: on: tags: true provider: releases - cleanup: false + skip_cleanup: true draft: true cache: cargo From 5adb4e54274cfa0a52278f2e784b49e0626d4615 Mon Sep 17 00:00:00 2001 From: chanderlud Date: Mon, 22 Jan 2024 16:58:47 -0800 Subject: [PATCH 2/2] update install functionality --- src/error.rs | 27 ++++++++++++++++- src/install/mod.rs | 74 ++++++++++++++++++++++++++++++++++++---------- src/main.rs | 8 ++++- src/options.rs | 19 ++++++++---- 4 files changed, 105 insertions(+), 23 deletions(-) diff --git a/src/error.rs b/src/error.rs index 5110126..4ad7763 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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)] @@ -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 for Error { @@ -193,6 +198,15 @@ impl From for Error { } } +#[cfg(feature = "installer")] +impl From 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 { @@ -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)] @@ -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"), } } } @@ -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 { diff --git a/src/install/mod.rs b/src/install/mod.rs index 58ed240..d7232f9 100644 --- a/src/install/mod.rs +++ b/src/install/mod.rs @@ -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"); @@ -27,17 +27,52 @@ pub(crate) async fn installer( client: Client, mut destination: PathBuf, custom_binary: Option, + 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 { @@ -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 @@ -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 { + 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?; @@ -170,7 +213,8 @@ async fn is_dir(client: &Client, os: &Os, path: &Path) -> Result { 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 { diff --git a/src/main.rs b/src/main.rs index 2beb610..d23b1b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); diff --git a/src/options.rs b/src/options.rs index f673355..88b7559 100644 --- a/src/options.rs +++ b/src/options.rs @@ -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)] @@ -194,12 +196,10 @@ 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, @@ -207,6 +207,13 @@ pub(crate) struct InstallOptions { /// Use a custom binary instead of downloading from Github #[clap(short, long)] pub(crate) custom_binary: Option, + + /// 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)]