Admin 08f42b7a32
Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled
feat(desktop): persist auth and install Linux updates in app
2026-09-24 18:36:34 +00:00

148 lines
4.1 KiB
Rust

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<String, ReleaseDownload>,
}
#[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::<u64>().unwrap_or(0))
.collect::<Vec<_>>()
};
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<UpdateInstallResult> {
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<UpdateInstallResult> {
Err(anyhow!("Linux DEB updates are only available on Linux"))
}