use std::{fs, path::PathBuf, process::Command, time::Duration}; use anyhow::{anyhow, Context, Result}; use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use uuid::Uuid; const RELEASE_FEED: &str = "https://git.arrowstack.dev/Admin/fieldlens-desktop/raw/branch/main/downloads/release.json"; const LINUX_DEB_URL: &str = "https://fieldlens.arrowstack.dev/downloads/fieldlens-print-linux-x64.deb"; #[derive(Debug, Deserialize)] struct ReleaseFeed { version: String, downloads: std::collections::HashMap, } #[derive(Debug, Deserialize)] struct ReleaseDownload { url: String, sha256: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct UpdateInstallResult { pub status: String, pub version: String, } fn newer(candidate: &str, current: &str) -> bool { let parse = |value: &str| { value .split('.') .map(|part| part.parse::().unwrap_or(0)) .collect::>() }; let left = parse(candidate); let right = parse(current); let width = left.len().max(right.len()); for index in 0..width { let a = *left.get(index).unwrap_or(&0); let b = *right.get(index).unwrap_or(&0); if a != b { return a > b; } } false } #[cfg(target_os = "linux")] pub fn install_linux_update() -> Result { let client = Client::builder() .timeout(Duration::from_secs(120)) .build() .context("create update HTTP client")?; let release: ReleaseFeed = client .get(RELEASE_FEED) .header("cache-control", "no-cache") .send() .context("fetch FieldLens release feed")? .error_for_status() .context("FieldLens release feed returned an error")? .json() .context("decode FieldLens release feed")?; if !newer(&release.version, env!("CARGO_PKG_VERSION")) { return Ok(UpdateInstallResult { status: "up-to-date".into(), version: release.version, }); } let download = release .downloads .get("linux-x86_64-deb") .ok_or_else(|| anyhow!("release feed has no Linux DEB"))?; if download.url != LINUX_DEB_URL { return Err(anyhow!("refusing unexpected Linux update URL")); } if download.sha256.len() != 64 || !download.sha256.chars().all(|value| value.is_ascii_hexdigit()) { return Err(anyhow!("release feed contains an invalid SHA-256")); } let package = client .get(&download.url) .send() .context("download FieldLens update")? .error_for_status() .context("FieldLens update download returned an error")? .bytes() .context("read FieldLens update")?; let actual = format!("{:x}", Sha256::digest(&package)); if !actual.eq_ignore_ascii_case(&download.sha256) { return Err(anyhow!("FieldLens update checksum verification failed")); } let path: PathBuf = std::env::temp_dir().join(format!( "fieldlens-print-update-{}-{}.deb", release.version, Uuid::new_v4() )); fs::write(&path, &package).context("stage FieldLens update")?; let installer = if std::path::Path::new("/usr/bin/apt-get").is_file() { "/usr/bin/apt-get" } else { "/usr/bin/dpkg" }; let mut command = Command::new("pkexec"); command.arg(installer); if installer.ends_with("apt-get") { command.args(["install", "-y"]).arg(&path); } else { command.arg("-i").arg(&path); } let status = command .status() .context("run elevated FieldLens update installer")?; let _ = fs::remove_file(&path); if !status.success() { return Err(anyhow!( "FieldLens update installation was cancelled or failed" )); } Ok(UpdateInstallResult { status: "installed".into(), version: release.version, }) } #[cfg(not(target_os = "linux"))] pub fn install_linux_update() -> Result { Err(anyhow!("Linux DEB updates are only available on Linux")) }