feat: initial version

This commit is contained in:
2025-12-17 20:19:43 +01:00
commit f7f3ac1b2e
9 changed files with 1880 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
/target
/testing
/www
Generated
+1530
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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),
}
}
}
+8
View File
@@ -0,0 +1,8 @@
mod arch;
mod errs;
mod updater;
mod versioning;
pub use errs::*;
pub use updater::*;
pub use versioning::*;
+21
View File
@@ -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
View File
@@ -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, &current_version) {
Ok(version) => version,
Err(err) => {
println!("failed to get latest version: {err}");
return;
}
};
if !latest_version.is_newer(&current_version) {
return;
}
if let Some(hook) = config.before_update {
hook(&current_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;
}
}
+87
View File
@@ -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())
}
}