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 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(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://github.com/{}/{}/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 tag = self.fetch_latest_tag().await?;
self.update_last_check_time();
let latest_version = tag.trim_start_matches('v').to_string();
if latest_version > self.version {
self.download_url = Some(format!(
"https://github.com/{}/{}/releases/download/{}/{}-{}-{}.tar.gz",
self.owner,
self.name,
tag,
self.name,
tag,
self.get_platform_identifier()
));
self.latest_version = Some(latest_version);
Ok(true)
} else {
Ok(false)
}
}
async fn fetch_latest_tag(&self) -> Result<String> {
let client = Client::builder().redirect(reqwest::redirect::Policy::none()).build()?;
let response = client.get(&self.releases_url).header("User-Agent", &self.name).send().await?;
let location = response
.headers()
.get(reqwest::header::LOCATION)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| msg_error_anyhow!(Message::UpdateLatestTagNotFound(self.releases_url.clone())))?;
match location.rsplit_once("/releases/tag/") {
Some((_, tag)) if !tag.is_empty() => Ok(tag.to_string()),
_ => Err(msg_error_anyhow!(Message::UpdateLatestTagNotFound(self.releases_url.clone()))),
}
}
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-gnu",
};
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,
}
}
}