use crate::libs::data_storage::DataStorage;
use crate::libs::messages::Message;
use crate::{msg_bail_anyhow, msg_error_anyhow, msg_info};
use anyhow::Result;
use chrono::{DateTime, Duration, Utc};
use flate2::read::GzDecoder;
use reqwest::Client;
use serde::Deserialize;
use std::env;
use std::fs::{self, File};
use std::path::PathBuf;
use tar::Archive;
include!(concat!(env!("OUT_DIR"), "/app_metadata.rs"));
const LAST_CHECK_FILE: &str = ".last_update_check";
const DAILY_CHECK_INTERVAL: i64 = 1;
const BACKUP_EXTENSION: &str = "bak";
#[derive(Deserialize, Debug)]
struct GitHubRelease {
tag_name: String,
assets: Vec<GitHubAsset>,
}
#[derive(Deserialize, Debug)]
struct GitHubAsset {
browser_download_url: String,
name: String,
}
#[derive(Debug)]
pub struct Updater {
pub client: Client,
pub owner: String,
pub name: String,
pub version: String,
pub latest_version: Option<String>,
pub download_url: Option<String>,
releases_url: String,
last_check_file: PathBuf,
}
impl Updater {
pub fn new() -> Result<Self> {
let owner = APP_METADATA_OWNER.to_owned();
let name = APP_METADATA_NAME.to_owned();
let last_check_file = DataStorage::new().get_path(LAST_CHECK_FILE)?;
let releases_url = format!("https://api.github.com/repos/{}/{}/releases/latest", owner, name);
Ok(Self {
client: Client::new(),
owner,
name,
version: APP_METADATA_VERSION.to_owned(),
latest_version: None,
download_url: None,
last_check_file,
releases_url,
})
}
pub async fn show_update_notification() {
let mut updater = match Self::new() {
Ok(up) => up,
Err(_) => return, };
if !updater.is_check_due() {
return;
}
if let Ok(true) = updater.check_for_latest_release().await
&& let Some(latest_version) = &updater.latest_version
{
msg_info!(
Message::UpdateAvailable {
app_name: updater.name,
latest: latest_version.to_string()
},
true )
}
}
pub async fn perform_update(&self) -> Result<()> {
let download_url = self.download_url.as_ref().ok_or(msg_error_anyhow!(Message::UpdateDownloadUrlNotSet))?;
let response = self.client.get(download_url).send().await?;
let content = response.bytes().await?;
let tar_gz_path = env::temp_dir().join(format!("{}.tar.gz", self.name));
fs::write(&tar_gz_path, &content)?;
self.extract_and_replace_binary(&tar_gz_path)?;
fs::remove_file(&tar_gz_path)?;
Ok(())
}
pub async fn check_for_latest_release(&mut self) -> Result<bool> {
let release = self.fetch_latest_github_release().await?;
self.update_last_check_time();
let latest_version = release.tag_name.trim_start_matches('v').to_string();
if latest_version > self.version {
self.latest_version = Some(latest_version);
self.download_url = self.find_platform_asset_url(&release.assets).map(|url| url.to_string());
Ok(true) } else {
Ok(false) }
}
async fn fetch_latest_github_release(&self) -> Result<GitHubRelease, reqwest::Error> {
self.client
.get(&self.releases_url)
.header("User-Agent", &self.name) .send()
.await?
.json::<GitHubRelease>()
.await
}
fn find_platform_asset_url<'a>(&self, assets: &'a [GitHubAsset]) -> Option<&'a str> {
let platform_name = self.get_platform_identifier();
assets
.iter()
.find(|asset| asset.name.contains(&platform_name))
.map(|asset| asset.browser_download_url.as_str())
}
fn extract_and_replace_binary(&self, tar_gz_path: &PathBuf) -> Result<()> {
let tar_gz = File::open(tar_gz_path)?;
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
let mut is_updated = false;
let current_exe = env::current_exe()?;
let current_exe_backup = current_exe.with_extension(BACKUP_EXTENSION);
for entry_result in archive.entries()? {
let mut entry = entry_result?;
let entry_path = entry.path()?;
if entry_path.ends_with(current_exe.file_name().unwrap()) {
fs::rename(¤t_exe, ¤t_exe_backup)?;
entry.unpack(¤t_exe)?;
is_updated = true;
} else {
let dest_path = current_exe.parent().unwrap().join(&entry_path);
entry.unpack(dest_path)?;
}
}
if is_updated {
Ok(())
} else {
msg_bail_anyhow!(Message::UpdateBinaryNotFoundInArchive);
}
}
fn get_platform_identifier(&self) -> String {
let arch = env::consts::ARCH;
let os = match env::consts::OS {
"windows" => "pc-windows-msvc",
"macos" => "apple-darwin",
_ => "unknown-linux-musl", };
format!("{}-{}", arch, os)
}
fn update_last_check_time(&self) {
let now = Utc::now().to_rfc3339();
let _ = fs::write(&self.last_check_file, now);
}
fn is_check_due(&self) -> bool {
match fs::read_to_string(&self.last_check_file) {
Ok(content) => {
let last_check = content.parse::<DateTime<Utc>>().unwrap_or_else(|_| {
Utc::now() - Duration::days(DAILY_CHECK_INTERVAL + 1)
});
Utc::now().signed_duration_since(last_check) > Duration::days(DAILY_CHECK_INTERVAL)
}
Err(_) => true, }
}
}