use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hyper_old_types::header::{LinkValue, RelationType};
use indicatif::ProgressStyle;
use reqwest::{self, header};
use serde_json;
use tempdir;
use crate::{confirm, errors::*, get_target, version, Download, Extract, Move, Status};
#[derive(Clone, Debug)]
pub struct ReleaseAsset {
pub download_url: String,
pub name: String,
}
impl ReleaseAsset {
fn from_asset(asset: &serde_json::Value) -> Result<ReleaseAsset> {
let download_url = asset["url"]
.as_str()
.ok_or_else(|| format_err!(Error::Release, "Asset missing `url`"))?;
let name = asset["name"]
.as_str()
.ok_or_else(|| format_err!(Error::Release, "Asset missing `name`"))?;
Ok(ReleaseAsset {
download_url: download_url.to_owned(),
name: name.to_owned(),
})
}
}
pub enum GitHubUpdateStatus {
UpToDate,
Updated(Release),
}
impl GitHubUpdateStatus {
pub fn into_status(self, current_version: String) -> Status {
match self {
GitHubUpdateStatus::UpToDate => Status::UpToDate(current_version),
GitHubUpdateStatus::Updated(release) => Status::Updated(release.version().into()),
}
}
pub fn uptodate(&self) -> bool {
match *self {
GitHubUpdateStatus::UpToDate => true,
_ => false,
}
}
pub fn updated(&self) -> bool {
!self.uptodate()
}
}
#[derive(Clone, Debug)]
pub struct Release {
pub name: String,
pub body: String,
pub tag: String,
pub date_created: String,
pub assets: Vec<ReleaseAsset>,
}
impl Release {
fn from_release(release: &serde_json::Value) -> Result<Release> {
let tag = release["tag_name"]
.as_str()
.ok_or_else(|| format_err!(Error::Release, "Release missing `tag_name`"))?;
let date_created = release["created_at"]
.as_str()
.ok_or_else(|| format_err!(Error::Release, "Release missing `created_at`"))?;
let name = release["name"].as_str().unwrap_or(tag);
let body = release["body"].as_str().unwrap_or("");
let assets = release["assets"]
.as_array()
.ok_or_else(|| format_err!(Error::Release, "No assets found"))?;
let assets = assets
.iter()
.map(ReleaseAsset::from_asset)
.collect::<Result<Vec<ReleaseAsset>>>()?;
Ok(Release {
name: name.to_owned(),
body: body.to_owned(),
tag: tag.to_owned(),
date_created: date_created.to_owned(),
assets,
})
}
pub fn has_target_asset(&self, target: &str) -> bool {
self.assets.iter().any(|asset| asset.name.contains(target))
}
pub fn asset_for(&self, target: &str) -> Option<ReleaseAsset> {
self.assets
.iter()
.filter(|asset| asset.name.contains(target))
.cloned()
.nth(0)
}
pub fn version(&self) -> &str {
self.tag.trim_start_matches('v')
}
}
#[derive(Clone, Debug)]
pub struct ReleaseListBuilder {
repo_owner: Option<String>,
repo_name: Option<String>,
target: Option<String>,
auth_token: Option<String>,
}
impl ReleaseListBuilder {
pub fn repo_owner(&mut self, owner: &str) -> &mut Self {
self.repo_owner = Some(owner.to_owned());
self
}
pub fn repo_name(&mut self, name: &str) -> &mut Self {
self.repo_name = Some(name.to_owned());
self
}
pub fn with_target(&mut self, target: &str) -> &mut Self {
self.target = Some(target.to_owned());
self
}
pub fn auth_token(&mut self, auth_token: &str) -> &mut Self {
self.auth_token = Some(auth_token.to_owned());
self
}
pub fn build(&self) -> Result<ReleaseList> {
Ok(ReleaseList {
repo_owner: if let Some(ref owner) = self.repo_owner {
owner.to_owned()
} else {
bail!(Error::Config, "`repo_owner` required")
},
repo_name: if let Some(ref name) = self.repo_name {
name.to_owned()
} else {
bail!(Error::Config, "`repo_name` required")
},
target: self.target.clone(),
auth_token: self.auth_token.clone(),
})
}
}
#[derive(Clone, Debug)]
pub struct ReleaseList {
repo_owner: String,
repo_name: String,
target: Option<String>,
auth_token: Option<String>,
}
impl ReleaseList {
pub fn configure() -> ReleaseListBuilder {
ReleaseListBuilder {
repo_owner: None,
repo_name: None,
target: None,
auth_token: None,
}
}
pub fn fetch(self) -> Result<Vec<Release>> {
set_ssl_vars!();
let api_url = format!(
"https://api.github.com/repos/{}/{}/releases",
self.repo_owner, self.repo_name
);
let releases = Self::fetch_releases(&api_url, &self.auth_token)?;
let releases = match self.target {
None => releases,
Some(ref target) => releases
.into_iter()
.filter(|r| r.has_target_asset(target))
.collect::<Vec<_>>(),
};
Ok(releases)
}
fn fetch_releases(url: &str, auth_token: &Option<String>) -> Result<Vec<Release>> {
let mut resp = reqwest::Client::new()
.get(url)
.headers(api_headers(&auth_token))
.send()?;
if !resp.status().is_success() {
bail!(
Error::Network,
"api request failed with status: {:?} - for: {:?}",
resp.status(),
url
)
}
let releases = resp.json::<serde_json::Value>()?;
let releases = releases
.as_array()
.ok_or_else(|| format_err!(Error::Release, "No releases found"))?;
let mut releases = releases
.iter()
.map(Release::from_release)
.collect::<Result<Vec<Release>>>()?;
let headers = resp.headers();
let links = headers.get_all(reqwest::header::LINK);
let next_link = links
.iter()
.filter_map(|link| {
if let Ok(link) = link.to_str() {
let lv = LinkValue::new(link.to_owned());
if let Some(rels) = lv.rel() {
if rels.contains(&RelationType::Next) {
return Some(link);
}
}
None
} else {
None
}
})
.nth(0);
Ok(match next_link {
None => releases,
Some(link) => {
releases.extend(Self::fetch_releases(link, &auth_token)?);
releases
}
})
}
}
#[derive(Debug)]
pub struct UpdateBuilder {
repo_owner: Option<String>,
repo_name: Option<String>,
target: Option<String>,
bin_name: Option<String>,
bin_install_path: Option<PathBuf>,
bin_path_in_archive: Option<PathBuf>,
show_download_progress: bool,
show_output: bool,
no_confirm: bool,
current_version: Option<String>,
target_version: Option<String>,
progress_style: Option<ProgressStyle>,
auth_token: Option<String>,
}
impl UpdateBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn repo_owner(&mut self, owner: &str) -> &mut Self {
self.repo_owner = Some(owner.to_owned());
self
}
pub fn repo_name(&mut self, name: &str) -> &mut Self {
self.repo_name = Some(name.to_owned());
self
}
pub fn current_version(&mut self, ver: &str) -> &mut Self {
self.current_version = Some(ver.to_owned());
self
}
pub fn target_version_tag(&mut self, ver: &str) -> &mut Self {
self.target_version = Some(ver.to_owned());
self
}
pub fn target(&mut self, target: &str) -> &mut Self {
self.target = Some(target.to_owned());
self
}
pub fn bin_name(&mut self, name: &str) -> &mut Self {
self.bin_name = Some(name.to_owned());
if self.bin_path_in_archive.is_none() {
self.bin_path_in_archive = Some(PathBuf::from(name));
}
self
}
pub fn bin_install_path<A: AsRef<Path>>(&mut self, bin_install_path: A) -> &mut Self {
self.bin_install_path = Some(PathBuf::from(bin_install_path.as_ref()));
self
}
pub fn bin_path_in_archive(&mut self, bin_path: &str) -> &mut Self {
self.bin_path_in_archive = Some(PathBuf::from(bin_path));
self
}
pub fn show_download_progress(&mut self, show: bool) -> &mut Self {
self.show_download_progress = show;
self
}
pub fn set_progress_style(&mut self, progress_style: ProgressStyle) -> &mut Self {
self.progress_style = Some(progress_style);
self
}
pub fn show_output(&mut self, show: bool) -> &mut Self {
self.show_output = show;
self
}
pub fn no_confirm(&mut self, no_confirm: bool) -> &mut Self {
self.no_confirm = no_confirm;
self
}
pub fn auth_token(&mut self, auth_token: &str) -> &mut Self {
self.auth_token = Some(auth_token.to_owned());
self
}
pub fn build(&self) -> Result<Update> {
let bin_install_path = if let Some(v) = &self.bin_install_path {
v.clone()
} else {
env::current_exe()?
};
Ok(Update {
repo_owner: if let Some(ref owner) = self.repo_owner {
owner.to_owned()
} else {
bail!(Error::Config, "`repo_owner` required")
},
repo_name: if let Some(ref name) = self.repo_name {
name.to_owned()
} else {
bail!(Error::Config, "`repo_name` required")
},
target: self
.target
.as_ref()
.map(|t| t.to_owned())
.unwrap_or_else(|| get_target().to_owned()),
bin_name: if let Some(ref name) = self.bin_name {
name.to_owned()
} else {
bail!(Error::Config, "`bin_name` required")
},
bin_install_path,
bin_path_in_archive: if let Some(ref path) = self.bin_path_in_archive {
path.to_owned()
} else {
bail!(Error::Config, "`bin_path_in_archive` required")
},
current_version: if let Some(ref ver) = self.current_version {
ver.to_owned()
} else {
bail!(Error::Config, "`current_version` required")
},
target_version: self.target_version.as_ref().map(|v| v.to_owned()),
show_download_progress: self.show_download_progress,
progress_style: self.progress_style.clone(),
show_output: self.show_output,
no_confirm: self.no_confirm,
auth_token: self.auth_token.clone(),
})
}
}
#[derive(Debug)]
pub struct Update {
repo_owner: String,
repo_name: String,
target: String,
current_version: String,
target_version: Option<String>,
bin_name: String,
bin_install_path: PathBuf,
bin_path_in_archive: PathBuf,
show_download_progress: bool,
show_output: bool,
no_confirm: bool,
progress_style: Option<ProgressStyle>,
auth_token: Option<String>,
}
impl Update {
pub fn configure() -> UpdateBuilder {
UpdateBuilder::new()
}
fn get_latest_release(
repo_owner: &str,
repo_name: &str,
auth_token: &Option<String>,
) -> Result<Release> {
set_ssl_vars!();
let api_url = format!(
"https://api.github.com/repos/{}/{}/releases/latest",
repo_owner, repo_name
);
let mut resp = reqwest::Client::new()
.get(&api_url)
.headers(api_headers(&auth_token))
.send()?;
if !resp.status().is_success() {
bail!(
Error::Network,
"api request failed with status: {:?} - for: {:?}",
resp.status(),
api_url
)
}
let json = resp.json::<serde_json::Value>()?;
Ok(Release::from_release(&json)?)
}
fn get_release_version(
repo_owner: &str,
repo_name: &str,
ver: &str,
auth_token: &Option<String>,
) -> Result<Release> {
set_ssl_vars!();
let api_url = format!(
"https://api.github.com/repos/{}/{}/releases/tags/{}",
repo_owner, repo_name, ver
);
let mut resp = reqwest::Client::new()
.get(&api_url)
.headers(api_headers(&auth_token))
.send()?;
if !resp.status().is_success() {
bail!(
Error::Network,
"api request failed with status: {:?} - for: {:?}",
resp.status(),
api_url
)
}
let json = resp.json::<serde_json::Value>()?;
Ok(Release::from_release(&json)?)
}
fn print_flush(&self, msg: &str) -> Result<()> {
if self.show_output {
print_flush!("{}", msg);
}
Ok(())
}
fn println(&self, msg: &str) {
if self.show_output {
println!("{}", msg);
}
}
pub fn update(self) -> Result<Status> {
let current_version = self.current_version.clone();
self.update_extended()
.map(|s| s.into_status(current_version))
}
pub fn update_extended(self) -> Result<GitHubUpdateStatus> {
self.println(&format!("Checking target-arch... {}", self.target));
self.println(&format!(
"Checking current version... v{}",
self.current_version
));
let release = match self.target_version {
None => {
self.print_flush("Checking latest released version... ")?;
let release =
Self::get_latest_release(&self.repo_owner, &self.repo_name, &self.auth_token)?;
{
let release_tag = release.version();
self.println(&format!("v{}", release_tag));
if !version::bump_is_greater(&self.current_version, &release_tag)? {
return Ok(GitHubUpdateStatus::UpToDate);
}
self.println(&format!(
"New release found! v{} --> v{}",
&self.current_version, release_tag
));
let qualifier =
if version::bump_is_compatible(&self.current_version, &release_tag)? {
""
} else {
"*NOT* "
};
self.println(&format!("New release is {}compatible", qualifier));
}
release
}
Some(ref ver) => {
self.println(&format!("Looking for tag: {}", ver));
Self::get_release_version(&self.repo_owner, &self.repo_name, ver, &self.auth_token)?
}
};
let target_asset = release.asset_for(&self.target).ok_or_else(|| {
format_err!(
Error::Release,
"No asset found for target: `{}`",
self.target
)
})?;
if self.show_output || !self.no_confirm {
println!("\n{} release status:", self.bin_name);
println!(" * Current exe: {:?}", self.bin_install_path);
println!(" * New exe release: {:?}", target_asset.name);
println!(" * New exe download url: {:?}", target_asset.download_url);
println!("\nThe new release will be downloaded/extracted and the existing binary will be replaced.");
}
if !self.no_confirm {
confirm("Do you want to continue? [Y/n] ")?;
}
let tmp_dir_parent = if cfg!(windows) {
env::var_os("TEMP").map(PathBuf::from)
} else {
self.bin_install_path.parent().map(PathBuf::from)
}
.ok_or_else(|| Error::Update("Failed to determine parent dir".into()))?;
let tmp_dir =
tempdir::TempDir::new_in(&tmp_dir_parent, &format!("{}_download", self.bin_name))?;
let tmp_archive_path = tmp_dir.path().join(&target_asset.name);
let mut tmp_archive = fs::File::create(&tmp_archive_path)?;
self.println("Downloading...");
let mut download = Download::from_url(&target_asset.download_url);
let mut headers = api_headers(&self.auth_token);
headers.insert(header::ACCEPT, "application/octet-stream".parse().unwrap());
download.set_headers(headers);
download.show_progress(self.show_download_progress);
if let Some(ref progress_style) = self.progress_style {
download.set_progress_style(progress_style.clone());
}
download.download_to(&mut tmp_archive)?;
self.print_flush("Extracting archive... ")?;
Extract::from_source(&tmp_archive_path)
.extract_file(&tmp_dir.path(), &self.bin_path_in_archive)?;
let new_exe = tmp_dir.path().join(&self.bin_path_in_archive);
self.println("Done");
self.print_flush("Replacing binary file... ")?;
let tmp_file = tmp_dir.path().join(&format!("__{}_backup", self.bin_name));
Move::from_source(&new_exe)
.replace_using_temp(&tmp_file)
.to_dest(&self.bin_install_path)?;
self.println("Done");
Ok(GitHubUpdateStatus::Updated(release))
}
}
impl Default for UpdateBuilder {
fn default() -> Self {
Self {
repo_owner: None,
repo_name: None,
target: None,
bin_name: None,
bin_install_path: None,
bin_path_in_archive: None,
show_download_progress: false,
show_output: true,
no_confirm: false,
current_version: None,
target_version: None,
progress_style: None,
auth_token: None,
}
}
}
fn api_headers(auth_token: &Option<String>) -> header::HeaderMap {
let mut headers = header::HeaderMap::new();
if auth_token.is_some() {
headers.insert(
header::AUTHORIZATION,
(String::from("token ") + &auth_token.clone().unwrap())
.parse()
.unwrap(),
);
};
headers
}