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