From 2465785a7a98134b2aeac642347e6f44037d5942 Mon Sep 17 00:00:00 2001 From: Joxit Date: Sat, 14 Oct 2023 10:43:45 +0200 Subject: [PATCH] feat(update): update your binary with `runtasktic update` --- .github/workflows/release.yml | 15 ++++ Cargo.toml | 1 + src/commands/mod.rs | 6 ++ src/commands/update.rs | 162 ++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 src/commands/update.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b50cf0a..24d04ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,11 @@ jobs: run: cargo install kokai - name: Create Release Note run: kokai release --ref ${{ github.ref }} --tag-from-ref . > RELEASE_NOTE.md + - name: Create sha256 + run: | + cd target/release + cp runtasktic runtasktic-linux-x86_64 + sha256sum -b runtasktic-linux-x86_64 > runtasktic-linux-x86_64.sha256 - name: Create Release id: create_release uses: actions/create-release@v1 @@ -47,3 +52,13 @@ jobs: asset_path: ./target/release/runtasktic asset_content_type: application/octet-stream asset_name: runtasktic-linux-x86_64 + - name: Upload SHA256 Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./target/release/runtasktic-linux-x86_64.sha256 + asset_content_type: application/octet-stream + asset_name: runtasktic-linux-x86_64.sha256 diff --git a/Cargo.toml b/Cargo.toml index bbf533a..6efc6ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ regex = "^1.3" chrono = "^0.4" cron = "0.12" clap_complete = "^4.4" +sha256 = "1.4" diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 09dd6d2..2af67b9 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,12 +2,14 @@ use crate::commands::completion::Completion; use crate::commands::dot::Dot; use crate::commands::exec::Exec; use crate::commands::run::Run; +use crate::commands::update::Update; use clap::Parser; mod completion; mod dot; mod exec; mod run; +mod update; #[derive(Parser, Debug)] pub enum Command { @@ -27,6 +29,9 @@ pub enum Command { /// Generate completion script for your shell. #[command(name = "completion", subcommand)] Completion(Completion), + /// Self update of the binary. + #[command(name = "update")] + Update(Update), } impl Command { @@ -36,6 +41,7 @@ impl Command { Command::Exec(executable) => executable.exec(), Command::Dot(executable) => executable.exec(), Command::Completion(executable) => executable.exec(), + Command::Update(executable) => executable.exec(), } } } diff --git a/src/commands/update.rs b/src/commands/update.rs new file mode 100644 index 0000000..d24e8b7 --- /dev/null +++ b/src/commands/update.rs @@ -0,0 +1,162 @@ +use clap::Parser; +use std::io::Write; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::prelude::OpenOptionsExt; +use std::{env, fs}; + +#[derive(Parser, Debug)] +pub struct Update {} + +impl Update { + pub fn exec(&self) { + if let Err(err) = self.update() { + eprintln!("{}", err); + } + } + + fn update(&self) -> Result<(), String> { + let path = env::current_exe().map_err(|msg| format!("Cannot find the executable: {}", msg))?; + let metadata = fs::metadata(&path) + .map_err(|msg| format!("Cannot find metadata of the executable: {}", msg))?; + if metadata.is_dir() + || metadata.is_symlink() + || !metadata.is_file() + || metadata.permissions().readonly() + { + return Err(format!("The executable cannot be replaced.")); + } + print!( + "The original executable has been located {}", + path.display() + ); + let new_path = path.with_extension("tmp"); + let mode = metadata.permissions().mode(); + let latest_version = Self::get_latest_version()?; + let binary = Self::get_binary(&latest_version)?; + let digest = Self::get_sha256(&latest_version)?; + let binary_digest = sha256::digest(&binary); + + if binary_digest != digest { + return Err(format!( + "Binary corrupted the downloaded sha256 does not match trusted: {} downloaded: {}", + digest, binary_digest + )); + } + + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .mode(mode) + .open(&new_path) + .map_err(|msg| format!("Cannot create binary on disk: {}", msg))?; + + file + .write_all(&binary) + .map_err(|msg| format!("Cannot write binary on disk: {}", msg))?; + + fs::rename(&new_path, &path).map_err(|msg| { + format!( + "Cannot rename {} to {}: {}", + new_path.display(), + path.display(), + msg + ) + })?; + + Ok(()) + } + + fn get_latest_version() -> Result { + let response = attohttpc::get("https://api.github.com/repos/Joxit/runtasktic/releases/latest") + .send() + .map_err(|msg| format!("Cannot get the latest version of the project: {}", msg))?; + + if !response.is_success() { + return Err(format!( + "Cannot get the latest version of the project: {}", + response.status() + )); + } + + let response_json = json::parse( + &response + .text() + .map_err(|msg| format!("Cannot get the GitHub API release: {}", msg))?, + ) + .map_err(|msg| format!("Cannot parse GitHub API release: {}", msg))?; + + let obj = match &response_json { + json::JsonValue::Object(obj) => obj, + _ => { + return Err(format!( + "Cannot get the latest version of the project: {}", + response_json + )) + } + }; + + let tag_name = obj.get("tag_name"); + + if let Some(body) = obj.get("body") { + println!("{}", body); + } + + let err = format!("The tag cannot be parsed"); + if let Some(test) = tag_name { + test.as_str().map(|version| version.to_string()).ok_or(err) + } else { + Err(err) + } + } + + fn get_binary(version: &String) -> Result, String> { + let url = format!( + "https://github.com/Joxit/runtasktic/releases/download/{}/runtasktic-linux-x86_64", + version + ); + let response = attohttpc::get(url) + .send() + .map_err(|msg| format!("Cannot get the binary of the project: {}", msg))?; + + if !response.is_success() { + return Err(format!( + "Cannot get the binary of the project: {}", + response.status() + )); + } + + let bytes = response + .bytes() + .map_err(|msg| format!("Cannot collect all the bytes of the binary: {}", msg))?; + + Ok(bytes) + } + + fn get_sha256(version: &String) -> Result { + let url = format!( + "https://github.com/Joxit/runtasktic/releases/download/{}/runtasktic-linux-x86_64.sha256", + version + ); + let response = attohttpc::get(url) + .send() + .map_err(|msg| format!("Cannot get the binary's sha256 of the project: {}", msg))?; + + if !response.is_success() { + return Err(format!( + "Cannot get the binary's sha256 of the project: {}", + response.status() + )); + } + + let sha256 = response + .text() + .map_err(|msg| format!("Cannot collect the binary's sha256 of the project: {}", msg))? + .trim() + .split_once(" ") + .unwrap_or_default() + .0 + .to_string(); + + Ok(sha256) + } +}