use std::path::Path;
use super::{fetch, InstallKind, Relaunch, Release, UpdateConfig, UpdateStage};
pub(crate) fn install(
config: &UpdateConfig,
release: &Release,
target: &Path,
on_stage: &dyn Fn(UpdateStage),
) -> Result<Relaunch, String> {
let asset = release
.asset_for(&InstallKind::AppImage(target.to_path_buf()))
.ok_or("this release hasn't published an AppImage for this architecture yet")?;
let name = target
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&config.slug);
let staged = target.with_file_name(format!(".{name}.update"));
let total = asset.size;
on_stage(UpdateStage::Downloading { done: 0, total });
let fetched = fetch::file(&asset.url, &staged, total, &config.user_agent, &|done| {
on_stage(UpdateStage::Downloading { done, total })
});
if let Err(e) = fetched {
let _ = std::fs::remove_file(&staged);
return Err(e);
}
on_stage(UpdateStage::Verifying);
if let Err(e) = config.verify_checksum(release, asset, &staged) {
let _ = std::fs::remove_file(&staged);
return Err(e);
}
on_stage(UpdateStage::Installing);
promote(&staged, target)
}
fn promote(staged: &Path, target: &Path) -> Result<Relaunch, String> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(staged, std::fs::Permissions::from_mode(0o755));
}
if let Err(e) = std::fs::rename(staged, target) {
let _ = std::fs::remove_file(staged);
return Err(format!("replace AppImage: {e}"));
}
Ok(Relaunch::Binary(target.to_path_buf()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::update::{ReleaseAsset, UpdateSource};
fn scratch(name: &str) -> std::path::PathBuf {
let dir =
std::env::temp_dir().join(format!("guise-update-image-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn config() -> UpdateConfig {
UpdateConfig::new("Acme", "1.0.0", UpdateSource::github("acme/acme"))
}
#[test]
fn promote_swaps_and_marks_executable() {
let dir = scratch("promote");
let target = dir.join("Acme.AppImage");
let staged = dir.join(".Acme.AppImage.update");
std::fs::write(&target, b"old").unwrap();
std::fs::write(&staged, b"new").unwrap();
let relaunch = promote(&staged, &target).unwrap();
assert_eq!(relaunch, Relaunch::Binary(target.clone()));
assert_eq!(std::fs::read(&target).unwrap(), b"new");
assert!(!staged.exists());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&target).unwrap().permissions().mode();
assert_eq!(mode & 0o755, 0o755);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn promote_fails_cleanly_without_a_staged_file() {
let dir = scratch("missing");
let err = promote(&dir.join("absent"), &dir.join("target")).unwrap_err();
assert!(err.contains("replace AppImage"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn promote_failure_drops_the_staged_file() {
let dir = scratch("promotefail");
let staged = dir.join(".Acme.AppImage.update");
std::fs::write(&staged, b"new").unwrap();
let err = promote(&staged, &dir.join("nosuchdir/Acme.AppImage")).unwrap_err();
assert!(err.contains("replace AppImage"));
assert!(!staged.exists());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn failed_download_leaves_no_staged_file() {
let dir = scratch("download");
let target = dir.join("Acme.AppImage");
let staged = dir.join(".Acme.AppImage.update");
std::fs::write(&staged, b"partial").unwrap();
let release = Release {
version: "9.9.9".to_string(),
url: String::new(),
assets: vec![ReleaseAsset {
name: format!("Acme-9.9.9-{}.AppImage", std::env::consts::ARCH),
url: "http://127.0.0.1/x".to_string(),
size: 0,
}],
};
assert!(install(&config(), &release, &target, &|_| {}).is_err());
assert!(!staged.exists());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_appimage_for_another_architecture_is_not_installed() {
let dir = scratch("otherarch");
let target = dir.join("Acme.AppImage");
std::fs::write(&target, b"working").unwrap();
let other = if std::env::consts::ARCH == "aarch64" {
"x86_64"
} else {
"aarch64"
};
let release = Release {
version: "9.9.9".to_string(),
url: String::new(),
assets: vec![ReleaseAsset {
name: format!("Acme-9.9.9-{other}.AppImage"),
url: "https://d/x".to_string(),
size: 0,
}],
};
assert!(install(&config(), &release, &target, &|_| {}).is_err());
assert_eq!(std::fs::read(&target).unwrap(), b"working");
let _ = std::fs::remove_dir_all(&dir);
}
}