1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use std::{
    env,
    fs::{self, File, Permissions},
    io,
    os::unix::fs::PermissionsExt,
    path::PathBuf,
    process::Command,
};

use anyhow::{Context, Result};
use chrono::Utc;
use displaydoc::Display;
use log::{debug, info, trace};
use serde_derive::Deserialize;
use thiserror::Error;

use self::UpdateSelfError as E;
use crate::{args::UpdateSelfOptions, tasks::ResolveEnv};

#[derive(Debug, Deserialize)]
struct GitHubReleaseJsonResponse {
    tag_name: String,
}

// Name user agent after the app, e.g. up-rs/1.2.3.
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");

impl ResolveEnv for UpdateSelfOptions {}

/// Downloads the latest version of the binary from the specified URL and
/// replaces the current executable path with it.
pub(crate) fn run(opts: &UpdateSelfOptions) -> Result<()> {
    let up_path = env::current_exe()?.canonicalize().unwrap();

    // If the current binary's location is where it was originally compiled, assume it is a dev
    // build, and thus skip the update.
    if !opts.always_update && up_path.starts_with(env!("CARGO_MANIFEST_DIR")) {
        info!(
            "Skipping up-rs update, current version '{}' is a dev build.",
            &up_path.display(),
        );
        return Ok(());
    }

    let client = reqwest::blocking::Client::builder()
        .user_agent(APP_USER_AGENT)
        .build()?;

    if opts.url == crate::args::SELF_UPDATE_URL {
        let latest_github_release = client
            .get(crate::args::LATEST_RELEASE_URL)
            .send()?
            .error_for_status()?
            .json::<GitHubReleaseJsonResponse>()?;
        trace!("latest_github_release: {:?}", latest_github_release,);
        let latest_github_release = latest_github_release.tag_name;
        if CURRENT_VERSION == latest_github_release {
            info!(
                "Skipping up-rs update, current version '{}' is latest GitHub version '{:?}'",
                CURRENT_VERSION, &latest_github_release,
            );
            return Ok(());
        }
    }

    let temp_dir = env::temp_dir();
    let temp_path = &temp_dir.join(format!("up_rs-{}", Utc::now().to_rfc3339()));

    debug!(
        "Downloading url {} to path {}",
        &opts.url,
        up_path.display()
    );

    debug!("Using temporary path: {}", temp_path.display());
    let mut response = reqwest::blocking::get(&opts.url)?.error_for_status()?;

    fs::create_dir_all(&temp_dir).with_context(|| E::CreateDir { path: temp_dir })?;
    let mut dest = File::create(&temp_path).with_context(|| E::CreateFile {
        path: temp_path.to_path_buf(),
    })?;
    io::copy(&mut response, &mut dest).context(E::Copy {})?;

    let permissions = Permissions::from_mode(0o755);
    fs::set_permissions(&temp_path, permissions).with_context(|| E::SetPermissions {
        path: temp_path.to_owned(),
    })?;

    let output = Command::new(temp_path).arg("--version").output()?;
    let new_version = String::from_utf8_lossy(&output.stdout);
    let new_version = new_version
        .trim()
        .trim_start_matches(concat!(env!("CARGO_PKG_NAME"), " "));
    if semver::Version::parse(new_version) > semver::Version::parse(CURRENT_VERSION) {
        info!(
            "Updating up-rs from '{}' to '{}'",
            CURRENT_VERSION, &new_version,
        );
        fs::rename(&temp_path, &up_path).with_context(|| E::Rename {
            from: temp_path.clone(),
            to: up_path.clone(),
        })?;
    } else {
        info!(
            "Skipping up-rs update, current version '{}' and new version '{}'",
            CURRENT_VERSION, &new_version,
        );
    }
    Ok(())
}

#[derive(Error, Debug, Display)]
/// Errors thrown by this file.
pub enum UpdateSelfError {
    /// Failed to create directory '{path}'
    CreateDir { path: PathBuf },
    /// Failed to create file '{path}'
    CreateFile { path: PathBuf },
    /// Failed to copy to destination file.
    Copy,
    /// Failed to set permissions for {path}.
    SetPermissions { path: PathBuf },
    /// Failed to rename {from} to {to}.
    Rename { from: PathBuf, to: PathBuf },
}