|
|
|
|
@@ -1,19 +1,20 @@
|
|
|
|
|
use semver::Version;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
use std::os::windows::process::CommandExt;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::{
|
|
|
|
|
fs,
|
|
|
|
|
process::{exit, Command as Process},
|
|
|
|
|
sync::mpsc::Sender,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use semver::Version;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "async")]
|
|
|
|
|
use async_std::channel::Sender as AsyncSender;
|
|
|
|
|
#[cfg(feature = "async")]
|
|
|
|
|
use async_std::task::JoinHandle;
|
|
|
|
|
|
|
|
|
|
use crate::bundle::Manifest;
|
|
|
|
|
use crate::{
|
|
|
|
|
locator::{self, LocationContext, VelopackLocator, VelopackLocatorConfig},
|
|
|
|
|
sources::UpdateSource,
|
|
|
|
|
@@ -82,12 +83,26 @@ pub struct VelopackAsset {
|
|
|
|
|
pub struct UpdateInfo {
|
|
|
|
|
/// The available version that we are updating to.
|
|
|
|
|
pub TargetFullRelease: VelopackAsset,
|
|
|
|
|
/// The base release that this update is based on. This is only available if the update is a delta update.
|
|
|
|
|
pub BaseRelease: Option<VelopackAsset>,
|
|
|
|
|
/// The list of delta updates that can be applied to the base version to get to the target version.
|
|
|
|
|
pub DeltasToTarget: Vec<VelopackAsset>,
|
|
|
|
|
/// True if the update is a version downgrade or lateral move (such as when switching channels to the same version number).
|
|
|
|
|
/// In this case, only full updates are allowed, and any local packages on disk newer than the downloaded version will be
|
|
|
|
|
/// deleted.
|
|
|
|
|
pub IsDowngrade: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl UpdateInfo {
|
|
|
|
|
pub(crate) fn new_full(target: VelopackAsset, is_downgrade: bool) -> UpdateInfo {
|
|
|
|
|
UpdateInfo { TargetFullRelease: target, BaseRelease: None, DeltasToTarget: Vec::new(), IsDowngrade: is_downgrade }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn new_delta(target: VelopackAsset, base: VelopackAsset, deltas: Vec<VelopackAsset>) -> UpdateInfo {
|
|
|
|
|
UpdateInfo { TargetFullRelease: target, BaseRelease: Some(base), DeltasToTarget: deltas, IsDowngrade: false }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AsRef<VelopackAsset> for UpdateInfo {
|
|
|
|
|
fn as_ref(&self) -> &VelopackAsset {
|
|
|
|
|
&self.TargetFullRelease
|
|
|
|
|
@@ -119,6 +134,9 @@ pub struct UpdateOptions {
|
|
|
|
|
/// allows you to explicitly switch channels, for example if the user wished to switch back to the 'stable' channel
|
|
|
|
|
/// without having to reinstall the application.
|
|
|
|
|
pub ExplicitChannel: Option<String>,
|
|
|
|
|
/// Sets the maximum number of deltas to consider before falling back to a full update.
|
|
|
|
|
/// The default is 10. Set to a negative number (eg. -1) to disable deltas.
|
|
|
|
|
pub MaximumDeltasBeforeFallback: i32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Provides functionality for checking for updates, downloading updates, and applying updates to the current application.
|
|
|
|
|
@@ -177,7 +195,11 @@ impl UpdateManager {
|
|
|
|
|
} else {
|
|
|
|
|
locator::auto_locate_app_manifest(LocationContext::FromCurrentExe)?
|
|
|
|
|
};
|
|
|
|
|
Ok(UpdateManager { options: options.unwrap_or_default(), source, locator })
|
|
|
|
|
let mut options = options.unwrap_or_default();
|
|
|
|
|
if options.MaximumDeltasBeforeFallback == 0 {
|
|
|
|
|
options.MaximumDeltasBeforeFallback = 10;
|
|
|
|
|
}
|
|
|
|
|
Ok(UpdateManager { options, source, locator })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_practical_channel(&self) -> String {
|
|
|
|
|
@@ -220,22 +242,26 @@ impl UpdateManager {
|
|
|
|
|
let packages_dir = self.locator.get_packages_dir();
|
|
|
|
|
if let Some((path, manifest)) = locator::find_latest_full_package(&packages_dir) {
|
|
|
|
|
if manifest.version > self.locator.get_manifest_version() {
|
|
|
|
|
return Some(VelopackAsset {
|
|
|
|
|
PackageId: manifest.id,
|
|
|
|
|
Version: manifest.version.to_string(),
|
|
|
|
|
Type: "Full".to_string(),
|
|
|
|
|
FileName: path.file_name().unwrap().to_string_lossy().to_string(),
|
|
|
|
|
SHA1: util::calculate_file_sha1(&path).unwrap_or_default(),
|
|
|
|
|
SHA256: util::calculate_file_sha256(&path).unwrap_or_default(),
|
|
|
|
|
Size: path.metadata().map(|m| m.len()).unwrap_or(0),
|
|
|
|
|
NotesMarkdown: manifest.release_notes,
|
|
|
|
|
NotesHtml: manifest.release_notes_html,
|
|
|
|
|
});
|
|
|
|
|
return Some(self.local_manifest_to_asset(&manifest, &path));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn local_manifest_to_asset(&self, manifest: &Manifest, path: &PathBuf) -> VelopackAsset {
|
|
|
|
|
VelopackAsset {
|
|
|
|
|
PackageId: manifest.id.clone(),
|
|
|
|
|
Version: manifest.version.to_string(),
|
|
|
|
|
Type: "Full".to_string(),
|
|
|
|
|
FileName: path.file_name().unwrap().to_string_lossy().to_string(),
|
|
|
|
|
SHA1: util::calculate_file_sha1(&path).unwrap_or_default(),
|
|
|
|
|
SHA256: util::calculate_file_sha256(&path).unwrap_or_default(),
|
|
|
|
|
Size: path.metadata().map(|m| m.len()).unwrap_or(0),
|
|
|
|
|
NotesMarkdown: manifest.release_notes.clone(),
|
|
|
|
|
NotesHtml: manifest.release_notes_html.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get a list of available remote releases from the package source.
|
|
|
|
|
pub fn get_release_feed(&self) -> Result<VelopackAssetFeed, Error> {
|
|
|
|
|
let channel = self.get_practical_channel();
|
|
|
|
|
@@ -266,9 +292,9 @@ impl UpdateManager {
|
|
|
|
|
return Ok(UpdateCheck::RemoteIsEmpty);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut latest: Option<VelopackAsset> = None;
|
|
|
|
|
let mut latest: Option<&VelopackAsset> = None;
|
|
|
|
|
let mut latest_version: Version = Version::parse("0.0.0")?;
|
|
|
|
|
for asset in assets {
|
|
|
|
|
for asset in &assets {
|
|
|
|
|
if let Ok(sv) = Version::parse(&asset.Version) {
|
|
|
|
|
if asset.Type.eq_ignore_ascii_case("Full") {
|
|
|
|
|
debug!("Found full release: {} ({}).", asset.FileName, sv.to_string());
|
|
|
|
|
@@ -291,21 +317,66 @@ impl UpdateManager {
|
|
|
|
|
|
|
|
|
|
if remote_version > app_version {
|
|
|
|
|
info!("Found newer remote release available ({} -> {}).", app_version, remote_version);
|
|
|
|
|
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: false }))
|
|
|
|
|
Ok(UpdateCheck::UpdateAvailable(self.create_delta_update_strategy(&assets, (remote_asset, remote_version))))
|
|
|
|
|
} else if remote_version < app_version && allow_downgrade {
|
|
|
|
|
info!("Found older remote release available and downgrade is enabled ({} -> {}).", app_version, remote_version);
|
|
|
|
|
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: true }))
|
|
|
|
|
Ok(UpdateCheck::UpdateAvailable(UpdateInfo::new_full(remote_asset.clone(), true)))
|
|
|
|
|
} else if remote_version == app_version && allow_downgrade && is_non_default_channel {
|
|
|
|
|
info!(
|
|
|
|
|
"Latest remote release is the same version of a different channel, and downgrade is enabled ({} -> {}, {} -> {}).",
|
|
|
|
|
app_version, remote_version, app_channel, practical_channel
|
|
|
|
|
);
|
|
|
|
|
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: true }))
|
|
|
|
|
Ok(UpdateCheck::UpdateAvailable(UpdateInfo::new_full(remote_asset.clone(), true)))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(UpdateCheck::NoUpdateAvailable)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_delta_update_strategy(
|
|
|
|
|
&self,
|
|
|
|
|
velopack_asset_feed: &Vec<VelopackAsset>,
|
|
|
|
|
latest_remote: (&VelopackAsset, Version),
|
|
|
|
|
) -> UpdateInfo {
|
|
|
|
|
let packages_dir = self.locator.get_packages_dir();
|
|
|
|
|
let latest_local = locator::find_latest_full_package(&packages_dir);
|
|
|
|
|
|
|
|
|
|
if latest_local.is_none() {
|
|
|
|
|
info!("There is no local/base package available for this update, so delta updates will be disabled.");
|
|
|
|
|
return UpdateInfo::new_full(latest_remote.0.clone(), false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (latest_local_path, latest_local_manifest) = latest_local.unwrap();
|
|
|
|
|
let local_asset = self.local_manifest_to_asset(&latest_local_manifest, &latest_local_path);
|
|
|
|
|
|
|
|
|
|
let assets_and_versions: Vec<(&VelopackAsset, Version)> =
|
|
|
|
|
velopack_asset_feed.iter().filter_map(|asset| Version::parse(&asset.Version).ok().map(|ver| (asset, ver))).collect();
|
|
|
|
|
|
|
|
|
|
let matching_latest_delta =
|
|
|
|
|
assets_and_versions.iter().find(|(asset, version)| asset.Type.eq_ignore_ascii_case("Delta") && version == &latest_remote.1);
|
|
|
|
|
|
|
|
|
|
if matching_latest_delta.is_none() {
|
|
|
|
|
info!("No matching delta update found for release {}, so deltas will be disabled.", latest_remote.1);
|
|
|
|
|
return UpdateInfo::new_full(latest_remote.0.clone(), false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut remotes_greater_than_local = assets_and_versions
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|(asset, _version)| asset.Type.eq_ignore_ascii_case("Delta"))
|
|
|
|
|
.filter(|(_asset, version)| version > &latest_local_manifest.version && version <= &latest_remote.1)
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
remotes_greater_than_local.sort_by(|a, b| a.1.cmp(&b.1));
|
|
|
|
|
let remotes_greater_than_local = remotes_greater_than_local.iter().map(|obj| obj.0.clone()).collect::<Vec<VelopackAsset>>();
|
|
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"Found {} delta updates between {} and {}.",
|
|
|
|
|
remotes_greater_than_local.len(),
|
|
|
|
|
latest_local_manifest.version,
|
|
|
|
|
latest_remote.1
|
|
|
|
|
);
|
|
|
|
|
UpdateInfo::new_delta(latest_remote.0.clone(), local_asset, remotes_greater_than_local)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Checks for updates, returning None if there are none available. If there are updates available, this method will return an
|
|
|
|
|
/// UpdateInfo object containing the latest available release, and any delta updates that can be applied if they are available.
|
|
|
|
|
#[cfg(feature = "async")]
|
|
|
|
|
@@ -327,7 +398,7 @@ impl UpdateManager {
|
|
|
|
|
|
|
|
|
|
fs::create_dir_all(packages_dir)?;
|
|
|
|
|
let final_target_file = packages_dir.join(name);
|
|
|
|
|
let partial_file = packages_dir.join(format!("{}.partial", name));
|
|
|
|
|
let partial_file = final_target_file.with_extension(".partial");
|
|
|
|
|
|
|
|
|
|
if final_target_file.exists() {
|
|
|
|
|
info!("Package already exists on disk, skipping download: '{}'", final_target_file.to_string_lossy());
|
|
|
|
|
@@ -336,6 +407,7 @@ impl UpdateManager {
|
|
|
|
|
|
|
|
|
|
let old_nupkg_pattern = format!("{}/*.nupkg", packages_dir.to_string_lossy());
|
|
|
|
|
let old_partial_pattern = format!("{}/*.partial", packages_dir.to_string_lossy());
|
|
|
|
|
let delta_pattern = format!("{}/-delta.nupkg", packages_dir.to_string_lossy());
|
|
|
|
|
let mut to_delete = Vec::new();
|
|
|
|
|
|
|
|
|
|
fn find_files_to_delete(pattern: &str, to_delete: &mut Vec<String>) {
|
|
|
|
|
@@ -355,12 +427,26 @@ impl UpdateManager {
|
|
|
|
|
find_files_to_delete(&old_nupkg_pattern, &mut to_delete);
|
|
|
|
|
find_files_to_delete(&old_partial_pattern, &mut to_delete);
|
|
|
|
|
|
|
|
|
|
self.source.download_release_entry(&update.TargetFullRelease, &partial_file.to_string_lossy(), progress)?;
|
|
|
|
|
info!("Successfully placed file: '{}'", partial_file.to_string_lossy());
|
|
|
|
|
if update.BaseRelease.is_some() && !update.DeltasToTarget.is_empty() {
|
|
|
|
|
info!("Beginning delta update process.");
|
|
|
|
|
if let Err(e) = self.download_and_apply_delta_updates(update, &partial_file, progress.clone()) {
|
|
|
|
|
error!("Error downloading delta updates: {}", e);
|
|
|
|
|
info!("Falling back to full update...");
|
|
|
|
|
self.source.download_release_entry(&update.TargetFullRelease, &partial_file.to_string_lossy(), progress)?;
|
|
|
|
|
self.verify_package_checksum(&partial_file, &update.TargetFullRelease)?;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.source.download_release_entry(&update.TargetFullRelease, &partial_file.to_string_lossy(), progress)?;
|
|
|
|
|
self.verify_package_checksum(&partial_file, &update.TargetFullRelease)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info!("Successfully downloaded file: '{}'", partial_file.to_string_lossy());
|
|
|
|
|
|
|
|
|
|
info!("Renaming partial file to final target: '{}'", final_target_file.to_string_lossy());
|
|
|
|
|
fs::rename(&partial_file, &final_target_file)?;
|
|
|
|
|
|
|
|
|
|
find_files_to_delete(&delta_pattern, &mut to_delete);
|
|
|
|
|
|
|
|
|
|
// extract new Update.exe on Windows only
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
match crate::bundle::load_bundle_from_file(&final_target_file) {
|
|
|
|
|
@@ -384,6 +470,63 @@ impl UpdateManager {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn download_and_apply_delta_updates(
|
|
|
|
|
&self,
|
|
|
|
|
update: &UpdateInfo,
|
|
|
|
|
target_file: &PathBuf,
|
|
|
|
|
progress: Option<Sender<i16>>,
|
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
|
let packages_dir = self.locator.get_packages_dir();
|
|
|
|
|
for (i, delta) in update.DeltasToTarget.iter().enumerate() {
|
|
|
|
|
let delta_file = packages_dir.join(&delta.FileName);
|
|
|
|
|
let partial_file = delta_file.with_extension("partial");
|
|
|
|
|
|
|
|
|
|
info!("Downloading delta package: '{}'", &delta.FileName);
|
|
|
|
|
self.source.download_release_entry(&delta, &partial_file.to_string_lossy(), None)?;
|
|
|
|
|
self.verify_package_checksum(&partial_file, delta)?;
|
|
|
|
|
|
|
|
|
|
fs::rename(&partial_file, &delta_file)?;
|
|
|
|
|
debug!("Successfully downloaded file: '{}'", &delta.FileName);
|
|
|
|
|
if let Some(progress) = &progress {
|
|
|
|
|
let _ = progress.send(((i as f64 / update.DeltasToTarget.len() as f64) * 70.0) as i16);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut args: Vec<String> =
|
|
|
|
|
["patch", "--old", &update.BaseRelease.as_ref().unwrap().FileName, "--output"].iter().map(|s| s.to_string()).collect();
|
|
|
|
|
args.push(target_file.to_string_lossy().to_string());
|
|
|
|
|
for delta in update.DeltasToTarget.iter() {
|
|
|
|
|
args.push("--delta".to_string());
|
|
|
|
|
let path = packages_dir.join(&delta.FileName);
|
|
|
|
|
args.push(path.to_string_lossy().to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info!("Applying {} patches to {}.", update.DeltasToTarget.len(), target_file.to_string_lossy());
|
|
|
|
|
|
|
|
|
|
let output = std::process::Command::new(self.locator.get_update_path()).args(args).output()?;
|
|
|
|
|
if output.status.success() {
|
|
|
|
|
info!("Successfully applied delta updates.");
|
|
|
|
|
} else {
|
|
|
|
|
let error_message = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
error!("Error applying delta updates: {}", error_message);
|
|
|
|
|
return Err(Error::Generic(error_message.to_string()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(progress) = &progress {
|
|
|
|
|
let _ = progress.send(100);
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn verify_package_checksum(&self, file: &PathBuf, asset: &VelopackAsset) -> Result<(), Error> {
|
|
|
|
|
let sha1 = util::calculate_file_sha1(file)?;
|
|
|
|
|
if !sha1.eq_ignore_ascii_case(&asset.SHA1) {
|
|
|
|
|
error!("SHA1 checksum mismatch for file '{}': expected '{}', got '{}'", file.to_string_lossy(), asset.SHA1, sha1);
|
|
|
|
|
return Err(Error::Checksum(file.to_string_lossy().to_string(), asset.SHA1.clone(), sha1));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Downloads the specified updates to the local app packages directory. Progress is reported back to the caller via an optional Sender.
|
|
|
|
|
/// This function will acquire a global update lock so may fail if there is already another update operation in progress.
|
|
|
|
|
/// - If the update contains delta packages and the delta feature is enabled
|
|
|
|
|
@@ -462,7 +605,7 @@ impl UpdateManager {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// This will launch the Velopack updater and optionally wait for a program to exit gracefully.
|
|
|
|
|
/// This method is unsafe because it does not necessarily wait for any / the correct process to exit
|
|
|
|
|
/// This method is unsafe because it does not necessarily wait for any / the correct process to exit
|
|
|
|
|
/// before applying updates. The `wait_exit_then_apply_updates` method is recommended for most use cases.
|
|
|
|
|
pub fn unsafe_apply_updates<A, C, S>(
|
|
|
|
|
&self,
|
|
|
|
|
|