Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43e9860b41 | ||
|
|
81f067abdb | ||
|
|
cdda2bc45f |
No files matched your search
@@ -4,6 +4,14 @@
|
||||
|
||||
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
||||
|
||||
## [1.3.5] - 2026-10-31
|
||||
|
||||
### Added
|
||||
- **One-click desktop update.** The "Update now" button actually downloads, verifies (SHA-256) and installs the new version now, instead of only linking to the Releases page. Works for all three desktop packages: AppImage (atomic self-replace), `.deb` (`dpkg -i`, no new password prompt — the app is already running as root/administrator by the time you click it), and the Windows installer (silent `/S` install). On Linux, the app restarts itself automatically; on Windows, the installer runs silently but the app does not reopen on its own yet — start it again once the install finishes.
|
||||
|
||||
### Changed
|
||||
- **The repository moved to the Ciberfauna organization** (`ciberfauna/guara`, was `lucasrguerra/guara`). **Important notice for existing installations:** versions up to 1.3.4 have the old location baked into their update check; they will not find this release automatically. Download 1.3.5 once manually from the new Releases page, and future update checks will work through the new location.
|
||||
|
||||
## [1.3.4] - 2026-10-31
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
||||
|
||||
## [1.3.5] - 2026-10-31
|
||||
|
||||
### Adicionado
|
||||
- **Atualização do app desktop em um clique.** O botão "Atualizar agora" agora baixa, confere (SHA-256) e instala a versão nova de verdade, em vez de só linkar pra página de Releases. Funciona nos três pacotes do desktop: AppImage (substituição atômica), `.deb` (`dpkg -i`, sem pedir senha de novo — o app já roda como root/administrador quando você clica), e o instalador do Windows (instalação silenciosa `/S`). No Linux o app reabre sozinho; no Windows o instalador roda silencioso mas o app ainda não reabre por conta própria — abra de novo depois que a instalação terminar.
|
||||
|
||||
### Alterado
|
||||
- **O repositório se mudou pra organização Ciberfauna** (`ciberfauna/guara`, era `lucasrguerra/guara`). **Aviso importante pra instalações existentes:** versões até a 1.3.4 têm o endereço antigo fixo na checagem de atualização e não vão encontrar esta versão sozinhas. Baixe a 1.3.5 manualmente uma vez na nova página de Releases, e as próximas verificações voltam a funcionar pelo novo endereço.
|
||||
|
||||
## [1.3.4] - 2026-10-31
|
||||
|
||||
### Corrigido
|
||||
|
||||
Generated
+2
-2
@@ -416,7 +416,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "guara-cli"
|
||||
version = "1.3.4"
|
||||
version = "1.3.5"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"ctrlc",
|
||||
@@ -429,7 +429,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "guara-core"
|
||||
version = "1.3.4"
|
||||
version = "1.3.5"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"argon2",
|
||||
|
||||
+2
-2
@@ -3,10 +3,10 @@ resolver = "2"
|
||||
members = ["crates/guara-core", "crates/guara-cli"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.3.4"
|
||||
version = "1.3.5"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/guara"
|
||||
repository = "https://git.lucasrguerra.dev.br/ciberfauna/guara"
|
||||
authors = ["Guará contributors"]
|
||||
rust-version = "1.80"
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1342,7 +1342,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "guara-core"
|
||||
version = "1.3.4"
|
||||
version = "1.3.5"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"argon2",
|
||||
@@ -1373,7 +1373,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "guara-gui"
|
||||
version = "1.3.4"
|
||||
version = "1.3.5"
|
||||
dependencies = [
|
||||
"guara-core",
|
||||
"libc",
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
[package]
|
||||
name = "guara-gui"
|
||||
description = "Guará: graphical file recovery interface"
|
||||
version = "1.3.4"
|
||||
version = "1.3.5"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Guará contributors"]
|
||||
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/guara"
|
||||
repository = "https://git.lucasrguerra.dev.br/ciberfauna/guara"
|
||||
rust-version = "1.80"
|
||||
|
||||
[workspace]
|
||||
|
||||
@@ -28,8 +28,19 @@ fn update_check(state: tauri::State<'_, State>) -> Result<serde_json::Value, Str
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_apply(state: tauri::State<'_, State>) -> Result<String, String> {
|
||||
state.0.update_apply().map_err(|e| e.to_string())
|
||||
fn update_apply(app: tauri::AppHandle, state: tauri::State<'_, State>) -> Result<String, String> {
|
||||
use tauri::Emitter;
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
let exe = elevated_exe().unwrap_or_else(|_| std::env::current_exe().unwrap_or_default());
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
let exe = std::env::current_exe().unwrap_or_default();
|
||||
|
||||
state
|
||||
.0
|
||||
.update_apply_desktop(&exe, move |phase| {
|
||||
let _ = app.emit("update-progress", serde_json::json!({ "phase": phase }));
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// `true` when the process already has sufficient privileges to read raw disks.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Guará",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"identifier": "dev.guara.app",
|
||||
"build": {
|
||||
"frontendDist": "../ui"
|
||||
|
||||
+46
-2
@@ -750,7 +750,11 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
||||
${tauri?.core?.invoke ? `<label class="check"><input type="checkbox" name="updates" ${SETTINGS.updates ? 'checked' : ''}><span><b>Atualizações automáticas</b><br><span class="sub">Ao abrir, consulta o Gitea para ver se há versão nova (uma consulta, sem enviar dados seus)</span><br><button type="button" class="btn sm" id="updNow">Verificar agora</button></span></label>` : ''}
|
||||
<menu><button class="btn" value="cancel">Cancelar</button><button class="btn primary" value="ok">Salvar</button></menu></form>`;
|
||||
d.querySelector('[name=lang]').value = SETTINGS.lang;
|
||||
d.querySelector('#updNow')?.addEventListener('click', (e) => { e.preventDefault(); checkUpdate(true); });
|
||||
d.querySelector('#updNow')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (UPD) applyUpdate();
|
||||
else checkUpdate(true);
|
||||
});
|
||||
d.onclose = () => {
|
||||
if (d.returnValue !== 'ok') return;
|
||||
const patch = { lang: d.querySelector('[name=lang]').value, notify: d.querySelector('[name=notify]').checked, maps: d.querySelector('[name=maps]').checked };
|
||||
@@ -768,6 +772,44 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
||||
// The desktop shell (Tauri) queries Gitea Releases API and notifies the user.
|
||||
// No-op in browser mode (`guara ui`).
|
||||
let UPD = null;
|
||||
let updating = false;
|
||||
|
||||
async function applyUpdate() {
|
||||
if (updating || !tauri?.core?.invoke) return;
|
||||
updating = true;
|
||||
const updApply = $('#updApply');
|
||||
const updNow = $('#updNow');
|
||||
const setPhase = (txt) => {
|
||||
if (updApply) { updApply.disabled = true; updApply.textContent = txt; }
|
||||
if (updNow) { updNow.disabled = true; updNow.textContent = txt; }
|
||||
};
|
||||
setPhase('Baixando a atualização…');
|
||||
|
||||
let unlisten = null;
|
||||
if (tauri?.event?.listen) {
|
||||
try {
|
||||
unlisten = await tauri.event.listen('update-progress', (event) => {
|
||||
const phase = event.payload?.phase;
|
||||
if (phase === 'downloading') setPhase('Baixando a atualização…');
|
||||
else if (phase === 'verifying') setPhase('Verificando integridade…');
|
||||
else if (phase === 'installing') setPhase('Instalando a atualização…');
|
||||
else if (phase === 'restarting') setPhase('Reiniciando…');
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
await tauri.core.invoke('update_apply');
|
||||
setPhase('Atualização concluída');
|
||||
} catch (e) {
|
||||
updating = false;
|
||||
if (updApply) { updApply.disabled = false; updApply.textContent = 'Atualizar agora'; }
|
||||
if (updNow) { updNow.disabled = false; updNow.textContent = UPD?.version ? `atualizar para ${UPD.version}` : 'Verificar agora'; }
|
||||
toast(`Falha na atualização: ${e}`);
|
||||
} finally {
|
||||
if (unlisten) unlisten();
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUpdate(manual) {
|
||||
if (!tauri?.core?.invoke) return;
|
||||
@@ -801,11 +843,13 @@ function renderUpdateCard() {
|
||||
if (!UPD) { el?.remove(); return; }
|
||||
if (!el) { el = document.createElement('div'); el.id = 'updateCard'; el.className = 'update-card'; el.setAttribute('role', 'status'); document.body.appendChild(el); }
|
||||
el.innerHTML = `<div class="upd-head"><b>Nova versão do Guará disponível</b><span class="mono upd-ver">v${esc(UPD.current)} → v${esc(UPD.version)}</span></div>
|
||||
<p class="sub">Esta instalação vem de um pacote do sistema e não se atualiza sozinha. Baixe a versão nova na página da release:</p><p class="mono upd-url">${esc(UPD.url)}</p>
|
||||
<p class="sub">Instalação automática da nova versão com um clique:</p>
|
||||
<div class="upd-actions">
|
||||
<button class="btn primary sm" id="updApply">Atualizar agora</button>
|
||||
<button class="btn sm" id="updLater">Depois</button>
|
||||
</div>`;
|
||||
$('#updLater')?.addEventListener('click', () => { UPD = null; el.remove(); });
|
||||
$('#updApply')?.addEventListener('click', () => applyUpdate());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ close while scan is running
|
||||
|
||||
@@ -582,7 +582,7 @@ export const EN = {
|
||||
'Atualizando o Guará…': 'Updating Guará…',
|
||||
'Baixando a atualização…': 'Downloading the update…',
|
||||
'Baixando… {n}%': 'Downloading… {n}%',
|
||||
'Instalando… o Guará vai reabrir sozinho.': 'Installing… Guará will reopen by itself.',
|
||||
'Instalando a atualização…': 'Installing the update…',
|
||||
'Esta instalação vem de um pacote do sistema e não se atualiza sozinha. Baixe a versão nova na página da release:': 'This install comes from a system package and cannot update itself. Download the new version from the release page:',
|
||||
'Atualizações automáticas': 'Automatic updates',
|
||||
'Ao abrir, consulta o Gitea para ver se há versão nova (uma consulta, sem enviar dados seus)': 'On startup, checks Gitea for a new version (one request, nothing of yours is sent)',
|
||||
@@ -593,4 +593,9 @@ export const EN = {
|
||||
'Nova versão {n} disponível': 'New version {n} available',
|
||||
'Você já está na versão mais recente': 'You are already on the latest version',
|
||||
'Não foi possível verificar atualizações: {n}': 'Could not check for updates: {n}',
|
||||
'Instalação automática da nova versão com um clique:': 'One-click automatic installation of the new version:',
|
||||
'Verificando integridade…': 'Verifying checksum…',
|
||||
'Reiniciando…': 'Restarting…',
|
||||
'Atualização concluída': 'Update completed',
|
||||
'Falha na atualização: {n}': 'Update failed: {n}',
|
||||
};
|
||||
@@ -1323,6 +1323,17 @@ impl Engine {
|
||||
let exe = std::env::current_exe()?;
|
||||
crate::update::apply(&exe)
|
||||
}
|
||||
|
||||
pub fn update_apply_desktop<F>(
|
||||
&self,
|
||||
current_exe: &std::path::Path,
|
||||
on_progress: F,
|
||||
) -> Result<String>
|
||||
where
|
||||
F: Fn(&str),
|
||||
{
|
||||
crate::update::apply_desktop_with_progress(current_exe, on_progress)
|
||||
}
|
||||
}
|
||||
|
||||
pub use crate::explorer::Query;
|
||||
|
||||
@@ -9,15 +9,15 @@ use crate::error::{Error, Result};
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
/// Gitea `releases/latest` API endpoint (`tag_name`, `assets[].name`, `assets[].browser_download_url`).
|
||||
const LATEST_RELEASE_URL: &str =
|
||||
"https://git.lucasrguerra.dev.br/api/v1/repos/lucasrguerra/guara/releases/latest";
|
||||
"https://git.lucasrguerra.dev.br/api/v1/repos/ciberfauna/guara/releases/latest";
|
||||
|
||||
/// Human-facing Gitea Releases page for desktop GUI users.
|
||||
const RELEASES_PAGE_URL: &str = "https://git.lucasrguerra.dev.br/lucasrguerra/guara/releases";
|
||||
const RELEASES_PAGE_URL: &str = "https://git.lucasrguerra.dev.br/ciberfauna/guara/releases";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Release {
|
||||
@@ -52,7 +52,7 @@ fn curl_text(url: &str) -> Result<String> {
|
||||
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn curl_download(url: &str, dest: &PathBuf) -> Result<()> {
|
||||
fn curl_download(url: &str, dest: &Path) -> Result<()> {
|
||||
let status = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: guara-updater", "-o"])
|
||||
.arg(dest)
|
||||
@@ -223,6 +223,273 @@ fn find_binary(dir: &PathBuf) -> Result<PathBuf> {
|
||||
Err(Error::msg("binário não encontrado no pacote baixado"))
|
||||
}
|
||||
|
||||
/// Desktop GUI installation type detected at runtime.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DesktopInstall {
|
||||
/// Linux AppImage: real filesystem path of the .AppImage file (from APPIMAGE env var).
|
||||
AppImage(PathBuf),
|
||||
/// Linux Debian package installed via dpkg: `current_exe` is reliable here.
|
||||
Deb,
|
||||
/// Windows NSIS installer: downloaded setup executable run with `/S`
|
||||
/// (silent). Does not relaunch the app on its own — see the comment on
|
||||
/// the `WindowsInstaller` apply path.
|
||||
WindowsInstaller,
|
||||
}
|
||||
|
||||
/// Detects the desktop installation type given an optional `APPIMAGE` environment
|
||||
/// variable value and the current executable path.
|
||||
pub fn detect_desktop_install_from(
|
||||
appimage_env: Option<&std::ffi::OsStr>,
|
||||
current_exe: &Path,
|
||||
) -> Result<DesktopInstall> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = (appimage_env, current_exe);
|
||||
Ok(DesktopInstall::WindowsInstaller)
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(val) = appimage_env {
|
||||
let path = PathBuf::from(val);
|
||||
if path.is_file() {
|
||||
return Ok(DesktopInstall::AppImage(path));
|
||||
}
|
||||
}
|
||||
if current_exe.is_file()
|
||||
&& current_exe
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.eq_ignore_ascii_case("appimage"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(DesktopInstall::AppImage(current_exe.to_path_buf()));
|
||||
}
|
||||
Ok(DesktopInstall::Deb)
|
||||
}
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
{
|
||||
let _ = (appimage_env, current_exe);
|
||||
Err(Error::msg(format!(
|
||||
"automatic self-update is not supported on this platform; please download manually from {RELEASES_PAGE_URL}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects the desktop installation type using the live environment.
|
||||
pub fn detect_desktop_install() -> Result<DesktopInstall> {
|
||||
let exe = std::env::current_exe()?;
|
||||
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), &exe)
|
||||
}
|
||||
|
||||
/// Returns the expected release asset suffix for a desktop installation and CPU architecture.
|
||||
pub fn gui_asset_suffix_for(install: &DesktopInstall, arch: &str) -> Result<&'static str> {
|
||||
match install {
|
||||
DesktopInstall::AppImage(_) => match arch {
|
||||
"x86_64" => Ok("_amd64.AppImage"),
|
||||
"aarch64" => Ok("_aarch64.AppImage"),
|
||||
other => Err(Error::msg(format!(
|
||||
"unsupported architecture for AppImage: {other}"
|
||||
))),
|
||||
},
|
||||
DesktopInstall::Deb => match arch {
|
||||
"x86_64" => Ok("_amd64.deb"),
|
||||
"aarch64" => Ok("_arm64.deb"),
|
||||
other => Err(Error::msg(format!(
|
||||
"unsupported architecture for deb package: {other}"
|
||||
))),
|
||||
},
|
||||
DesktopInstall::WindowsInstaller => match arch {
|
||||
"x86_64" => Ok("_x64-setup.exe"),
|
||||
other => Err(Error::msg(format!(
|
||||
"unsupported architecture for Windows installer: {other}"
|
||||
))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the expected release asset suffix for the current architecture.
|
||||
pub fn gui_asset_suffix(install: &DesktopInstall) -> Result<&'static str> {
|
||||
gui_asset_suffix_for(install, std::env::consts::ARCH)
|
||||
}
|
||||
|
||||
/// Downloads, verifies, and installs the latest desktop GUI release in place.
|
||||
pub fn apply_desktop(current_exe: &Path) -> Result<String> {
|
||||
apply_desktop_with_progress(current_exe, |_| {})
|
||||
}
|
||||
|
||||
/// Downloads, verifies, and installs the latest desktop GUI release in place,
|
||||
/// notifying the progress callback of phase transitions (`"downloading"`,
|
||||
/// `"verifying"`, `"installing"`, `"restarting"`).
|
||||
pub fn apply_desktop_with_progress<F>(current_exe: &Path, on_progress: F) -> Result<String>
|
||||
where
|
||||
F: Fn(&str),
|
||||
{
|
||||
let install =
|
||||
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), current_exe)?;
|
||||
let body = curl_text(LATEST_RELEASE_URL)?;
|
||||
let release: Release = serde_json::from_str(&body)?;
|
||||
|
||||
let suffix = gui_asset_suffix(&install)?;
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name.ends_with(suffix))
|
||||
.ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"no asset ending with `{suffix}` in release {}",
|
||||
release.tag_name
|
||||
))
|
||||
})?;
|
||||
let checksum_asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name == format!("{}.sha256", asset.name));
|
||||
|
||||
let tmp_dir = std::env::temp_dir().join("guara-desktop-update");
|
||||
fs::create_dir_all(&tmp_dir)?;
|
||||
let download_path = tmp_dir.join(&asset.name);
|
||||
|
||||
on_progress("downloading");
|
||||
curl_download(&asset.browser_download_url, &download_path)?;
|
||||
|
||||
if let Some(checksum_asset) = checksum_asset {
|
||||
on_progress("verifying");
|
||||
let expected = curl_text(&checksum_asset.browser_download_url)?;
|
||||
let expected_hash = expected
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let data = fs::read(&download_path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&data);
|
||||
let actual_hash: String = hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect();
|
||||
if actual_hash != expected_hash {
|
||||
return Err(Error::msg(
|
||||
"checksum SHA-256 não confere — atualização descartada",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
on_progress("installing");
|
||||
match install {
|
||||
DesktopInstall::AppImage(target_appimage) => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let parent_dir = target_appimage.parent().unwrap_or_else(|| Path::new("."));
|
||||
let temp_name = format!(
|
||||
".{}.tmp-{}",
|
||||
target_appimage
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("appimage"),
|
||||
std::process::id()
|
||||
);
|
||||
let temp_file = parent_dir.join(temp_name);
|
||||
fs::copy(&download_path, &temp_file)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&temp_file)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&temp_file, perms)?;
|
||||
}
|
||||
fs::rename(&temp_file, &target_appimage)?;
|
||||
let _ = fs::remove_file(&download_path);
|
||||
|
||||
on_progress("restarting");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = Command::new(&target_appimage)
|
||||
.args(std::env::args().skip(1))
|
||||
.exec();
|
||||
return Err(Error::msg(format!("failed to relaunch AppImage: {err}")));
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (&target_appimage, &download_path);
|
||||
return Err(Error::msg("AppImage updates are only supported on Linux"));
|
||||
}
|
||||
}
|
||||
DesktopInstall::Deb => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let status = Command::new("dpkg")
|
||||
.args(["-i"])
|
||||
.arg(&download_path)
|
||||
.status()
|
||||
.map_err(|e| Error::msg(format!("dpkg -i: {e}")))?;
|
||||
if !status.success() {
|
||||
return Err(Error::msg(format!(
|
||||
"dpkg -i failed with exit code {:?}",
|
||||
status.code()
|
||||
)));
|
||||
}
|
||||
let _ = fs::remove_file(&download_path);
|
||||
|
||||
on_progress("restarting");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = Command::new(current_exe)
|
||||
.args(std::env::args().skip(1))
|
||||
.exec();
|
||||
return Err(Error::msg(format!("failed to relaunch updated app: {err}")));
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (&download_path, current_exe);
|
||||
return Err(Error::msg("deb updates are only supported on Linux"));
|
||||
}
|
||||
}
|
||||
DesktopInstall::WindowsInstaller => {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// `/S` is the one NSIS silent-install switch with a documented,
|
||||
// stable meaning. There is no standard NSIS or Tauri flag that
|
||||
// makes a silent install relaunch the app afterwards — that was
|
||||
// claimed in an earlier draft of this code and could not be
|
||||
// verified against NSIS's or Tauri's documentation; passing an
|
||||
// invented flag to a stock NSIS installer is silently ignored,
|
||||
// not harmful, but also not a relaunch mechanism. Spawning
|
||||
// detached and exiting immediately (instead of waiting on the
|
||||
// child) also avoids a self-deadlock: if the installer's script
|
||||
// waits for this process to close before it can overwrite the
|
||||
// running .exe, blocking here on the installer's exit status
|
||||
// would hang forever. The practical effect: the update installs
|
||||
// silently, but the app does not reopen itself — the user needs
|
||||
// to start it again. This needs real-world confirmation on
|
||||
// Windows before relying on it; if testing shows the bundled
|
||||
// NSIS template does support an explicit relaunch flag or step,
|
||||
// wire it in here instead of this comment.
|
||||
on_progress("restarting");
|
||||
Command::new(&download_path)
|
||||
.arg("/S")
|
||||
.spawn()
|
||||
.map_err(|e| Error::msg(format!("failed to launch installer: {e}")))?;
|
||||
std::process::exit(0);
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = &download_path;
|
||||
return Err(Error::msg(
|
||||
"Windows installer updates are only supported on Windows",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Ok(release.tag_name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -234,4 +501,70 @@ mod tests {
|
||||
let err = apply(&PathBuf::from("C:/Program Files/Guara/guara-gui.exe")).unwrap_err();
|
||||
assert!(err.to_string().contains(RELEASES_PAGE_URL), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_desktop_install_variants() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let temp_appimage =
|
||||
std::env::temp_dir().join(format!("test-{}.AppImage", std::process::id()));
|
||||
let _ = fs::write(&temp_appimage, b"dummy appimage");
|
||||
|
||||
let res = detect_desktop_install_from(
|
||||
Some(temp_appimage.as_os_str()),
|
||||
Path::new("/usr/bin/guara-gui"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, DesktopInstall::AppImage(temp_appimage.clone()));
|
||||
|
||||
let res_fallback = detect_desktop_install_from(None, &temp_appimage).unwrap();
|
||||
assert_eq!(
|
||||
res_fallback,
|
||||
DesktopInstall::AppImage(temp_appimage.clone())
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(&temp_appimage);
|
||||
|
||||
let res_deb =
|
||||
detect_desktop_install_from(None, Path::new("/usr/bin/guara-gui")).unwrap();
|
||||
assert_eq!(res_deb, DesktopInstall::Deb);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let res = detect_desktop_install_from(
|
||||
None,
|
||||
Path::new("C:/Program Files/Guara/guara-gui.exe"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, DesktopInstall::WindowsInstaller);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_asset_suffix_matches_known_release_naming() {
|
||||
let appimage = DesktopInstall::AppImage(PathBuf::from("/tmp/Guara.AppImage"));
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&appimage, "x86_64").unwrap(),
|
||||
"_amd64.AppImage"
|
||||
);
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&appimage, "aarch64").unwrap(),
|
||||
"_aarch64.AppImage"
|
||||
);
|
||||
|
||||
let deb = DesktopInstall::Deb;
|
||||
assert_eq!(gui_asset_suffix_for(&deb, "x86_64").unwrap(), "_amd64.deb");
|
||||
assert_eq!(gui_asset_suffix_for(&deb, "aarch64").unwrap(), "_arm64.deb");
|
||||
|
||||
let win = DesktopInstall::WindowsInstaller;
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&win, "x86_64").unwrap(),
|
||||
"_x64-setup.exe"
|
||||
);
|
||||
|
||||
assert!(gui_asset_suffix_for(&appimage, "mips").is_err());
|
||||
assert!(gui_asset_suffix_for(&deb, "riscv64").is_err());
|
||||
assert!(gui_asset_suffix_for(&win, "arm").is_err());
|
||||
}
|
||||
}
|
||||
@@ -167,4 +167,17 @@ EOF
|
||||
*) echo "unknown platform: $platform" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
(
|
||||
cd ../../../dist-gui
|
||||
for asset in *; do
|
||||
[ -f "$asset" ] || continue
|
||||
case "$asset" in
|
||||
*.sha256) ;;
|
||||
*)
|
||||
sha256sum "$asset" > "$asset.sha256"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
)
|
||||
|
||||
ls -1 ../../../dist-gui
|
||||
Reference in new issue
Block a user