feat: initial version
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
/testing
|
||||
/www
|
||||
Generated
+1530
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "updater"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2.178"
|
||||
reqwest = { version = "0.12.26", features = ["blocking"] }
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
pub(crate) const TARGET_ARCH: &str = "amd64";
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
pub(crate) const TARGET_ARCH: &str = "arm64";
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) const TARGET_ARCH: &str = "wasm32";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const TARGET_OS: &str = "linux";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) const TARGET_OS: &str = "windows";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) const TARGET_OS: &str = "macos";
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
|
||||
pub enum UpdateError {
|
||||
ParseCurrentVersion,
|
||||
ParseLatestVersion,
|
||||
FetchLatestVersion,
|
||||
FetchLatestVersionStatus { status: StatusCode },
|
||||
FetchLatestVersionDoesNotExist,
|
||||
VersionMajorMismatch,
|
||||
Other { message: &'static str },
|
||||
}
|
||||
|
||||
impl Display for UpdateError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self {
|
||||
UpdateError::ParseCurrentVersion => write!(f, "failed to parse current version"),
|
||||
UpdateError::ParseLatestVersion => write!(f, "failed to parse latest version"),
|
||||
UpdateError::FetchLatestVersion => write!(f, "failed to fetch latest version"),
|
||||
UpdateError::FetchLatestVersionStatus { status } => write!(
|
||||
f,
|
||||
"failed to fetch latest version with status code {}",
|
||||
status
|
||||
),
|
||||
UpdateError::FetchLatestVersionDoesNotExist => {
|
||||
write!(f, "there is no update directory for this major version")
|
||||
}
|
||||
UpdateError::VersionMajorMismatch => {
|
||||
write!(f, "version major mismatch in download meta data")
|
||||
}
|
||||
UpdateError::Other { message } => write!(f, "{}", message),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mod arch;
|
||||
mod errs;
|
||||
mod updater;
|
||||
mod versioning;
|
||||
|
||||
pub use errs::*;
|
||||
pub use updater::*;
|
||||
pub use versioning::*;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use std::env;
|
||||
|
||||
use updater::Config;
|
||||
|
||||
const APP_NAME: &str = "myapp";
|
||||
const APP_DIR: &str = "/home/coder/projects/updater/testing";
|
||||
const SERVER_URL: &str = "http://localhost:3000";
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() {
|
||||
updater::exec(Config {
|
||||
app_name: APP_NAME,
|
||||
app_dir: APP_DIR,
|
||||
server_url: SERVER_URL,
|
||||
before_update: Some(|current_version, latest_version| {
|
||||
println!("Updating from {current_version} to {latest_version}")
|
||||
}),
|
||||
});
|
||||
|
||||
println!("Application runs in version {}", VERSION)
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
use std::{
|
||||
env,
|
||||
ffi::CString,
|
||||
fs::{self, File},
|
||||
os::unix::fs::{PermissionsExt, symlink},
|
||||
};
|
||||
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::{arch, errs::UpdateError, versioning::Version};
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const NO_UPDATE_ENV_VAR: &str = "NO_UPDATE";
|
||||
|
||||
pub struct Config {
|
||||
pub app_name: &'static str,
|
||||
pub app_dir: &'static str,
|
||||
pub server_url: &'static str,
|
||||
pub before_update: Option<fn(&Version, &Version) -> ()>,
|
||||
}
|
||||
|
||||
fn get_current_version() -> Result<Version, UpdateError> {
|
||||
let version = match Version::try_from(VERSION) {
|
||||
Ok(version) => version,
|
||||
Err(_) => return Err(UpdateError::ParseCurrentVersion),
|
||||
};
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
fn get_latest_version(config: &Config, current: &Version) -> Result<Version, UpdateError> {
|
||||
let url = format!(
|
||||
"{}/{}/{}/v{}/version",
|
||||
config.server_url,
|
||||
arch::TARGET_OS,
|
||||
arch::TARGET_ARCH,
|
||||
current.major
|
||||
);
|
||||
let response = match reqwest::blocking::get(url) {
|
||||
Ok(response) => response,
|
||||
Err(_) => return Err(UpdateError::FetchLatestVersion),
|
||||
};
|
||||
match response.status() {
|
||||
StatusCode::OK => {}
|
||||
StatusCode::NOT_FOUND => {
|
||||
return Err(UpdateError::FetchLatestVersionDoesNotExist);
|
||||
}
|
||||
status => {
|
||||
return Err(UpdateError::FetchLatestVersionStatus { status });
|
||||
}
|
||||
};
|
||||
|
||||
let text = response.text().map_err(|_| UpdateError::Other {
|
||||
message: "failed to parse server response text",
|
||||
})?;
|
||||
let latest = match Version::try_from(text.trim()) {
|
||||
Ok(version) => version,
|
||||
Err(_) => return Err(UpdateError::ParseLatestVersion),
|
||||
};
|
||||
|
||||
if current.major != latest.major {
|
||||
return Err(UpdateError::VersionMajorMismatch);
|
||||
}
|
||||
|
||||
Ok(latest)
|
||||
}
|
||||
|
||||
fn download_file(url: &str, file_name: &str) -> Result<(), &'static str> {
|
||||
let mut response =
|
||||
reqwest::blocking::get(url).map_err(|_| "Could not fetch newest version from server")?;
|
||||
|
||||
if response.status() != StatusCode::OK {
|
||||
return Err("Failed to download file");
|
||||
}
|
||||
|
||||
let mut file =
|
||||
File::create(file_name).map_err(|_| "Could not create file for downloaded version")?;
|
||||
std::io::copy(&mut response, &mut file)
|
||||
.map_err(|_| "Could not write newest version to file")?;
|
||||
|
||||
let mut perms = fs::metadata(&file_name)
|
||||
.map_err(|_| "failed to get permissions for downloaded file")?
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&file_name, perms)
|
||||
.map_err(|_| "failed to write permissions to downloaded file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_sym_link(config: &Config, dest_file: &str) -> Result<(), &'static str> {
|
||||
let tmp_link = format!("{}/.{}_temp", config.app_dir, config.app_name);
|
||||
let link_path = format!("{}/{}", config.app_dir, config.app_name);
|
||||
|
||||
let _ = fs::remove_file(&tmp_link);
|
||||
symlink(dest_file, &tmp_link).map_err(|_| "failed to create temp symlink")?;
|
||||
fs::rename(&tmp_link, link_path).map_err(|_| "failed to overwrite symlink")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exec_new_file(config: &Config) -> Result<(), &'static str> {
|
||||
let path = CString::new(format!("{}/{}", config.app_dir, config.app_name))
|
||||
.map_err(|_| "error with CString for path")?;
|
||||
|
||||
let arg0 = CString::new(config.app_name).map_err(|_| "error with CString for path")?;
|
||||
let args = [arg0.as_ptr(), std::ptr::null()];
|
||||
|
||||
unsafe {
|
||||
env::set_var(NO_UPDATE_ENV_VAR, "1");
|
||||
libc::execv(path.as_ptr(), args.as_ptr());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec(config: Config) {
|
||||
if let Ok(_) = env::var(NO_UPDATE_ENV_VAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_version = match get_current_version() {
|
||||
Ok(version) => version,
|
||||
Err(err) => {
|
||||
println!("failed to get current version: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let latest_version = match get_latest_version(&config, ¤t_version) {
|
||||
Ok(version) => version,
|
||||
Err(err) => {
|
||||
println!("failed to get latest version: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !latest_version.is_newer(¤t_version) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(hook) = config.before_update {
|
||||
hook(¤t_version, &latest_version);
|
||||
}
|
||||
|
||||
let download_url = format!(
|
||||
"{}/{}/{}/v{}/bin",
|
||||
config.server_url,
|
||||
arch::TARGET_OS,
|
||||
arch::TARGET_ARCH,
|
||||
latest_version.major
|
||||
);
|
||||
let download_file_name = format!(
|
||||
"{}/{}_{}",
|
||||
config.app_dir,
|
||||
config.app_name,
|
||||
latest_version.str()
|
||||
);
|
||||
if let Err(err) = download_file(&download_url, &download_file_name) {
|
||||
println!("failed to download latest version: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = make_sym_link(&config, &download_file_name) {
|
||||
println!("failed to create symlink: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = exec_new_file(&config) {
|
||||
println!("failed to create symlink: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct Version {
|
||||
pub major: u32,
|
||||
pub minor: u32,
|
||||
pub patch: u32,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
pub fn is_newer(&self, other: &Version) -> bool {
|
||||
if self.major > other.major {
|
||||
return true;
|
||||
}
|
||||
if self.minor > other.minor {
|
||||
return true;
|
||||
}
|
||||
if self.patch > other.patch {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn str(&self) -> String {
|
||||
format!("v{}.{}.{}", self.major, self.minor, self.patch)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Version {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let value = match value.strip_prefix("v") {
|
||||
Some(value) => value,
|
||||
None => value,
|
||||
};
|
||||
|
||||
let parts: Vec<u32> = value
|
||||
.split(".")
|
||||
.map(|part| part.parse::<u32>().map_err(|_| "Version has invalid digit"))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if parts.len() != 3 {
|
||||
return Err("Only versions with 3 parts are allowed");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
major: parts[0],
|
||||
minor: parts[1],
|
||||
patch: parts[2],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Version {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.major.cmp(&other.major) {
|
||||
std::cmp::Ordering::Equal => {}
|
||||
ord => return ord,
|
||||
}
|
||||
match self.minor.cmp(&other.minor) {
|
||||
std::cmp::Ordering::Equal => {}
|
||||
ord => return ord,
|
||||
}
|
||||
self.patch.cmp(&other.patch)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Version {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
match self.major.partial_cmp(&other.major) {
|
||||
Some(std::cmp::Ordering::Equal) => {}
|
||||
ord => return ord,
|
||||
}
|
||||
match self.minor.partial_cmp(&other.minor) {
|
||||
Some(std::cmp::Ordering::Equal) => {}
|
||||
ord => return ord,
|
||||
}
|
||||
self.patch.partial_cmp(&other.patch)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Version {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.str())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user