added auto update & changed config path
This commit is contained in:
168
src/updater.rs
Normal file
168
src/updater.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
// src/updater.rs
|
||||
use anyhow::{Context, Result};
|
||||
use futures::StreamExt;
|
||||
use serde::Deserialize;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpdateInfo {
|
||||
pub current_version: String,
|
||||
pub new_version: String,
|
||||
pub download_url: String,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Release {
|
||||
tag_name: String,
|
||||
prerelease: bool,
|
||||
assets: Vec<ReleaseAsset>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseAsset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
/// Checks Gitea releases API for latest stable (non-prerelease) release.
|
||||
/// Returns Ok(None) if already up to date or no matching asset found.
|
||||
pub async fn check(api_base: &str, timeout_sec: u64) -> Result<Option<UpdateInfo>> {
|
||||
let url = format!("{}/releases?limit=10&page=1", api_base);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(timeout_sec))
|
||||
.build()
|
||||
.context("failed to build HTTP client")?;
|
||||
|
||||
let releases: Vec<Release> = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to fetch releases")?
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse releases JSON")?;
|
||||
|
||||
let current_version = env!("CARGO_PKG_VERSION").to_string();
|
||||
|
||||
// Find the latest non-prerelease release
|
||||
let latest = releases.into_iter().find(|r| !r.prerelease);
|
||||
let release = match latest {
|
||||
Some(r) => r,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Strip "v" prefix and anything after "@" (e.g. "v1.0.0@master" → "1.0.0")
|
||||
let tag = release.tag_name.as_str();
|
||||
let tag = tag.strip_prefix('v').unwrap_or(tag);
|
||||
let tag = tag.split('@').next().unwrap_or(tag).trim();
|
||||
let new_version = tag.to_string();
|
||||
|
||||
// Already up to date
|
||||
if new_version == current_version {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Find a matching asset
|
||||
let asset_prefix = format!(
|
||||
"{}-{}-{}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH
|
||||
);
|
||||
|
||||
let asset = release
|
||||
.assets
|
||||
.into_iter()
|
||||
.find(|a| a.name.starts_with(&asset_prefix));
|
||||
|
||||
let asset = match asset {
|
||||
Some(a) => a,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
Ok(Some(UpdateInfo {
|
||||
current_version,
|
||||
new_version,
|
||||
download_url: asset.browser_download_url,
|
||||
size: asset.size,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Downloads binary to temp file with streaming progress, atomically replaces
|
||||
/// current exe, sets chmod 755.
|
||||
/// progress_tx receives bytes downloaded so far.
|
||||
pub async fn download_and_apply(
|
||||
info: &UpdateInfo,
|
||||
progress_tx: UnboundedSender<u64>,
|
||||
) -> Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let response = client
|
||||
.get(&info.download_url)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to start download")?
|
||||
.error_for_status()
|
||||
.context("download request failed with error status")?;
|
||||
|
||||
// Write to a temp file next to the current exe so rename is atomic
|
||||
let current_exe = std::env::current_exe().context("failed to get current exe path")?;
|
||||
let exe_dir = current_exe
|
||||
.parent()
|
||||
.context("current exe has no parent directory")?;
|
||||
|
||||
let tmp_path = exe_dir.join(format!(".{}.tmp", env!("CARGO_PKG_NAME")));
|
||||
|
||||
let mut file = tokio::fs::File::create(&tmp_path)
|
||||
.await
|
||||
.context("failed to create temp file")?;
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut downloaded: u64 = 0;
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.context("error reading download chunk")?;
|
||||
downloaded += chunk.len() as u64;
|
||||
file.write_all(&chunk)
|
||||
.await
|
||||
.context("failed to write to temp file")?;
|
||||
let _ = progress_tx.send(downloaded);
|
||||
}
|
||||
|
||||
// Flush and close
|
||||
drop(file);
|
||||
|
||||
// chmod 755
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let perms = std::fs::Permissions::from_mode(0o755);
|
||||
std::fs::set_permissions(&tmp_path, perms).context("failed to set permissions")?;
|
||||
}
|
||||
|
||||
// Atomically replace current exe
|
||||
std::fs::rename(&tmp_path, ¤t_exe).context("failed to replace current exe")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replaces current process via execv (Unix). Never returns on success.
|
||||
#[cfg(unix)]
|
||||
pub fn exec_updated() -> ! {
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
let exe = std::env::current_exe().expect("failed to get current exe path");
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
let err = std::process::Command::new(&exe)
|
||||
.args(&args[1..])
|
||||
.exec();
|
||||
|
||||
// exec only returns if it failed
|
||||
eprintln!("Failed to exec updated binary: {}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user