use std::ffi::OsString;
use std::path::Path;
use std::sync::mpsc;
use corundum_framework_platform::package;
use semver::Version;
use velopack::locator::{LocationContext, VelopackLocatorConfig, auto_locate_app_manifest};
use velopack::sources::{GithubSource, HttpSource};
use velopack::{UpdateCheck, UpdateInfo, UpdateManager};
use crate::error::Error;
use crate::updater::{After, Source};
#[allow(clippy::large_enum_variant)]
pub(crate) enum Applied {
Downloaded(UpdateInfo),
Pending,
}
pub(crate) trait Backend: Send + Sync {
fn current(&self) -> Version;
fn check(&self) -> Result<Option<UpdateInfo>, Error>;
fn download(&self, info: &UpdateInfo, progress: &dyn Fn(u8)) -> Result<(), Error>;
fn pending(&self) -> Option<Version>;
fn apply(&self, applied: Applied, after: &After) -> Result<(), Error>;
}
pub(crate) struct Velopack {
manager: UpdateManager,
}
impl Velopack {
pub(crate) fn new(source: &Source) -> Result<Velopack, Error> {
let located = locate()?.config;
let manager = match source {
Source::Github { repo, prerelease } => {
url::Url::parse(repo.trim_end_matches('/')).map_err(|e| {
Error::Velopack(format!("the repository {repo:?} is not a URL: {e}"))
})?;
UpdateManager::new(GithubSource::new(repo, None, *prerelease), None, located)
}
Source::Http(url) => UpdateManager::new(HttpSource::new(url), None, located),
}
.map_err(from_velopack)?;
Ok(Velopack { manager })
}
}
pub(crate) struct Located {
pub(crate) config: Option<VelopackLocatorConfig>,
pub(crate) version: Version,
}
pub(crate) fn locate() -> Result<Located, Error> {
let exe = package::current_exe().ok_or(Error::NotPackaged)?;
locate_from(&exe)
}
pub(crate) fn locate_from(exe: &Path) -> Result<Located, Error> {
let locator = auto_locate_app_manifest(LocationContext::FromSpecifiedAppExecutable(
exe.to_path_buf(),
))
.map_err(from_velopack)?;
let version = locator.get_manifest_version();
#[cfg(windows)]
let config = None;
#[cfg(not(windows))]
let config = {
let bin_dir = locator.get_current_bin_dir();
#[cfg(target_os = "linux")]
let root = locator.get_appimage_path();
#[cfg(not(target_os = "linux"))]
let root = locator.get_root_dir();
let manifest = [
bin_dir.join("sq.version"),
root.join("Contents").join("Resources").join("sq.version"),
]
.into_iter()
.find(|path| path.exists())
.unwrap_or_else(|| bin_dir.join("sq.version"));
Some(VelopackLocatorConfig {
RootAppDir: root,
UpdateExePath: locator.get_update_path(),
PackagesDir: locator.get_packages_dir(),
ManifestPath: manifest,
CurrentBinaryDir: bin_dir,
IsPortable: locator.get_is_portable(),
})
};
Ok(Located { config, version })
}
impl Backend for Velopack {
fn current(&self) -> Version {
self.manager.get_current_version()
}
fn check(&self) -> Result<Option<UpdateInfo>, Error> {
match self.manager.check_for_updates().map_err(from_velopack)? {
UpdateCheck::UpdateAvailable(info) => Ok(Some(*info)),
UpdateCheck::RemoteIsEmpty | UpdateCheck::NoUpdateAvailable => Ok(None),
}
}
fn download(&self, info: &UpdateInfo, progress: &dyn Fn(u8)) -> Result<(), Error> {
let (tx, rx) = mpsc::channel::<i16>();
std::thread::scope(|scope| {
let download = scope.spawn(move || self.manager.download_updates(info, Some(tx)));
for percent in rx {
progress(percent.clamp(0, 100) as u8);
}
download
.join()
.unwrap_or_else(|_| Err(velopack::Error::Other("the download panicked".into())))
})
.map_err(from_velopack)
}
fn pending(&self) -> Option<Version> {
let asset = self.manager.get_update_pending_restart()?;
Version::parse(&asset.Version).ok()
}
fn apply(&self, applied: Applied, after: &After) -> Result<(), Error> {
let asset = match applied {
Applied::Downloaded(info) => info.TargetFullRelease,
Applied::Pending => self
.manager
.get_update_pending_restart()
.ok_or_else(|| Error::Velopack("nothing downloaded".into()))?,
};
let (restart, args): (bool, &[OsString]) = match after {
After::RestartApp => (true, &[]),
After::RestartWith(args) => (true, args),
After::Exit => (false, &[]),
};
self.manager
.wait_exit_then_apply_updates(&asset, false, restart, args)
.map_err(from_velopack)?;
if matches!(after, After::Exit) {
std::process::exit(0);
}
Ok(())
}
}
fn from_velopack(e: velopack::Error) -> Error {
match e {
velopack::Error::NotInstalled(_) => Error::NotPackaged,
velopack::Error::Network(e) => Error::Network(e.to_string()),
e => Error::Velopack(e.to_string()),
}
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use std::fs;
use velopack::locator::VelopackLocator;
use super::*;
#[test]
fn a_located_appimage_gives_velopack_paths_it_accepts() {
let tmp = tempfile::tempdir().unwrap();
let tmp = tmp.path().canonicalize().unwrap();
let bin = tmp.join("mount/usr/bin");
fs::create_dir_all(&bin).unwrap();
fs::write(bin.join("UpdateNix"), "").unwrap();
fs::write(
bin.join("sq.version"),
"<package><metadata><id>demo</id><version>1.2.3</version>\
<mainExe>demo</mainExe></metadata></package>",
)
.unwrap();
fs::write(bin.join("demo"), "").unwrap();
let image = tmp.join("Demo.AppImage");
fs::write(&image, "").unwrap();
unsafe { std::env::set_var("APPIMAGE", &image) };
let located = locate_from(&bin.join("demo")).unwrap();
assert_eq!(located.version, Version::new(1, 2, 3));
let config = located.config.unwrap();
assert_eq!(config.RootAppDir, image);
assert_eq!(config.ManifestPath, bin.join("sq.version"));
let locator = VelopackLocator::new(&config).unwrap();
assert_eq!(locator.get_manifest_version(), Version::new(1, 2, 3));
assert_eq!(locator.get_main_exe_path(), bin.join("demo"));
}
}