use bytes::Bytes;
use colour::{green, green_ln_bold, magenta, magenta_ln_bold, red, yellow_ln_bold};
use core::str;
use directories::BaseDirs;
use log::{error, info, warn};
use reqwest::{self, Client};
use std::{
env,
error::Error,
fmt::Display,
fs,
io::{self, Write},
path::PathBuf,
process, thread,
time::Duration,
};
use zip;
#[derive(Debug)]
struct CleanupError {
message: String,
}
#[derive(Debug)]
struct UpdateError {
message: String,
}
impl Display for UpdateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Update Error: {}", self.message)
}
}
impl Display for CleanupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Cleanup Error: {}", self.message)
}
}
impl Error for UpdateError {}
impl Error for CleanupError {}
pub async fn check_for_update() -> Result<(), Box<dyn Error>> {
info!("Checking for updates...\n");
let get_prior_update = process::Command::new("cmd")
.args(["/C", "set PRIOR_UPDATE"])
.output();
match get_prior_update {
Ok(output) => {
let output_string = str::from_utf8(&output.stdout)?;
if output_string.contains("1") {
additional_cleanup()?
}
}
Err(e) => {
warn!("issue getting PRIOR_UPDATE: {e}");
}
}
let client = Client::new();
let response = client
.get("https://github.com/OneilNvM/rl-hours-tracker/releases/latest")
.send()
.await?;
let url = response.url().to_string();
let url_vec: Vec<&str> = url.rsplit("/").collect();
let version = url_vec[0].replace("v", "");
if version == env!("CARGO_PKG_VERSION") {
yellow_ln_bold!("Latest Version: {version}");
Ok(())
} else {
let mut option = String::new();
magenta_ln_bold!("NEW VERSION AVAILABLE!!\n");
magenta!("Update to version '{version}' ");
print!("(");
green!("y");
print!(" / ");
red!("n");
print!("): ");
std::io::stdout()
.flush()
.unwrap_or_else(|_| println!("Update to version '{version}' (y/n)?"));
io::stdin().read_line(&mut option)?;
if option.trim().to_lowercase() == "y" {
yellow_ln_bold!("\nDownloading update...\n");
update(&version).await?;
Ok(())
} else {
Ok(())
}
}
}
pub async fn update(ver_num: &str) -> Result<(), Box<dyn Error>> {
let client = Client::new();
let url = format!(
"https://github.com/OneilNvM/rl-hours-tracker/releases/download/v{ver_num}/update.zip"
);
let response = client.get(url).send().await?;
if !response.status().is_success() {
yellow_ln_bold!("The newest update includes changes to the built-in updater.");
thread::sleep(Duration::from_secs(3));
yellow_ln_bold!("You will need to download the newest installer from GitHub.");
thread::sleep(Duration::from_secs(5));
process::exit(0)
}
let download = response.bytes().await?;
let base_dir = BaseDirs::new();
if base_dir.is_none() {
error!("base dir returned None");
return Err(Box::new(UpdateError {
message: String::from("base dir returned None"),
}));
}
let app_dir = base_dir
.unwrap()
.config_local_dir()
.join("Programs")
.join("Rocket League Hours Tracker");
let tmp_result = fs::create_dir(app_dir.join("tmp"));
if tmp_result.is_err() {
error!("error creating tmp directory.\ncreating zip file locally.\n");
extract_local_zip(&app_dir, &download)?;
} else {
extract_update(app_dir, download)?;
}
green_ln_bold!("Update complete!\n");
thread::sleep(Duration::from_millis(1000));
yellow_ln_bold!("Please wait for the program to close...");
thread::sleep(Duration::from_millis(5000));
let set_prior_update = process::Command::new("cmd")
.args(["/C", "setx", "PRIOR_UPDATE", "1"])
.status();
if let Err(e) = set_prior_update {
warn!("issue setting up PRIOR_UPDATE: {e}");
}
process::exit(0)
}
fn additional_cleanup() -> Result<(), Box<dyn Error>> {
info!("Starting additional cleanup of previous version");
let base_dir = BaseDirs::new();
if base_dir.is_none() {
error!("base dir returned None");
return Err(Box::new(CleanupError {
message: String::from("base dir returned None"),
}));
}
let app_dir = base_dir
.unwrap()
.config_local_dir()
.join("Programs")
.join("Rocket League Hours Tracker");
fs::remove_file(app_dir.join("old-rl-hours-tracker.exe"))?;
let change_prior_update = process::Command::new("cmd")
.args(["/C", "setx", "PRIOR_UPDATE", "0"])
.status();
if let Err(e) = change_prior_update {
warn!("issue changing PRIOR_UPDATE: {e}");
}
info!("Cleanup successful");
Ok(())
}
fn extract_update(app_dir: PathBuf, download: Bytes) -> Result<(), Box<dyn Error>> {
yellow_ln_bold!("Created 'tmp' directory...");
let file_name = app_dir.join("tmp").join("update.zip");
fs::write(file_name, download)?;
yellow_ln_bold!("Downloaded 'update.zip' archive...");
fs::rename(
app_dir.join("rl-hours-tracker.exe"),
app_dir.join("old-rl-hours-tracker.exe"),
)?;
yellow_ln_bold!("Removing old files...");
fs::remove_file(app_dir.join("unins000.dat"))?;
fs::remove_file(app_dir.join("unins000.exe"))?;
let update = fs::File::open(app_dir.join("tmp\\update.zip"))?;
yellow_ln_bold!("Extracting update files...");
let mut archive = zip::ZipArchive::new(update)?;
archive.extract(&app_dir)?;
yellow_ln_bold!("Update files extracted");
fs::remove_dir_all(app_dir.join("tmp"))?;
yellow_ln_bold!("Removed tmp directory...\n");
Ok(())
}
fn extract_local_zip(app_dir: &PathBuf, download: &Bytes) -> Result<(), Box<dyn Error>> {
let file_name = app_dir.join("update.zip");
fs::write(file_name, download)?;
yellow_ln_bold!("Downloaded 'update.zip' archive locally...");
fs::rename(
app_dir.join("rl-hours-tracker.exe"),
app_dir.join("old-rl-hours-tracker.exe"),
)?;
fs::remove_file(app_dir.join("unins000.dat"))?;
fs::remove_file(app_dir.join("unins000.exe"))?;
yellow_ln_bold!("Removing old files...");
let update = fs::File::open(app_dir.join("update.zip"))?;
yellow_ln_bold!("Extracting update files...");
let mut archive = zip::ZipArchive::new(update)?;
archive.extract(app_dir)?;
yellow_ln_bold!("Update files extracted");
fs::remove_file(app_dir.join("update.zip"))?;
yellow_ln_bold!("Removed 'update.zip' archive...\n");
Ok(())
}