runtasktic 1.2.1

Command-line task management tool for execution of regular long sequential or parallel tasks.
Documentation
use anyhow::{Context, Result};
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) -> Result<()> {
    self.update()
  }

  fn update(&self) -> Result<()> {
    let path = env::current_exe().with_context(|| "Cannot find the executable")?;
    let metadata = fs::metadata(&path)
      .with_context(|| anyhow!("Cannot find metadata of the executable {}", path.display()))?;
    if metadata.is_dir()
      || metadata.is_symlink()
      || !metadata.is_file()
      || metadata.permissions().readonly()
    {
      bail!("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);

    ensure!(
      binary_digest == digest,
      "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)
      .with_context(|| anyhow!("Cannot create binary {} on disk", new_path.display()))?;

    file
      .write_all(&binary)
      .with_context(|| anyhow!("Cannot write binary {} on disk", new_path.display()))?;

    fs::rename(&new_path, &path)
      .with_context(|| anyhow!("Cannot rename {} to {}", new_path.display(), path.display()))?;

    Ok(())
  }

  fn get_latest_version() -> Result<String> {
    let response = attohttpc::get("https://api.github.com/repos/Joxit/runtasktic/releases/latest")
      .send()
      .with_context(|| "Cannot get the latest version of the project")?;

    ensure!(
      response.is_success(),
      "Cannot get the latest version of the project from GitHub API: {}",
      response.status()
    );

    let response_json = json::parse(
      &response
        .text()
        .with_context(|| "Cannot get the GitHub API release")?,
    )
    .with_context(|| "Cannot parse GitHub API release")?;

    let obj = match &response_json {
      json::JsonValue::Object(obj) => obj,
      _ => {
        bail!(
          "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 = anyhow!("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<Vec<u8>> {
    let url = format!(
      "https://github.com/Joxit/runtasktic/releases/download/{}/runtasktic-linux-x86_64",
      version
    );
    let response = attohttpc::get(url)
      .send()
      .with_context(|| format!("Cannot get the binary version {} of the project", version))?;

    ensure!(
      response.is_success(),
      "Cannot get the binary version {} of the project: {}",
      version,
      response.status()
    );

    let bytes = response.bytes().with_context(|| {
      anyhow!(
        "Cannot collect all the bytes of the binary version {}",
        version
      )
    })?;

    Ok(bytes)
  }

  fn get_sha256(version: &String) -> Result<String> {
    let url = format!(
      "https://github.com/Joxit/runtasktic/releases/download/{}/runtasktic-linux-x86_64.sha256",
      version
    );
    let response = attohttpc::get(url).send().with_context(|| {
      anyhow!(
        "Cannot get the binary's sha256 of the project version {}",
        version
      )
    })?;

    ensure!(
      response.is_success(),
      "Cannot get the binary's sha256 of the project version {}: {}",
      version,
      response.status()
    );

    let sha256 = response
      .text()
      .with_context(|| {
        anyhow!(
          "Cannot collect the binary's sha256 of the project version {}",
          version
        )
      })?
      .trim()
      .split_once(" ")
      .unwrap_or_default()
      .0
      .to_string();

    Ok(sha256)
  }
}