2024-01-31 11:55:55 +00:00
using System ;
2024-09-20 22:57:28 -06:00
using System.ComponentModel ;
2023-12-21 13:15:34 +00:00
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Logging.Abstractions ;
using NuGet.Versioning ;
2023-12-31 11:09:44 +00:00
using Velopack.Compression ;
2025-01-10 11:37:20 +00:00
using Velopack.Exceptions ;
2023-12-31 11:09:44 +00:00
using Velopack.Locators ;
2024-01-01 13:03:44 +00:00
using Velopack.NuGet ;
2023-12-31 11:09:44 +00:00
using Velopack.Sources ;
2024-09-21 12:48:05 -06:00
using Velopack.Util ;
2023-12-21 13:15:34 +00:00
2023-12-31 11:09:44 +00:00
namespace Velopack
2023-12-21 13:15:34 +00:00
{
2023-12-24 12:42:19 +00:00
/// <summary>
/// Provides functionality for checking for updates, downloading updates, and applying updates to the current application.
/// </summary>
2024-01-31 11:55:55 +00:00
public partial class UpdateManager
2023-12-21 13:15:34 +00:00
{
2023-12-31 11:23:38 +00:00
/// <summary> The currently installed application Id. This would be what you set when you create your release.</summary>
2024-01-16 16:41:59 +00:00
public virtual string? AppId => Locator . AppId ;
2023-12-21 13:15:34 +00:00
2023-12-24 12:42:19 +00:00
/// <summary> True if this application is currently installed, and is able to download/check for updates. </summary>
2023-12-21 13:15:34 +00:00
public virtual bool IsInstalled => Locator . CurrentlyInstalledVersion != null ;
2024-03-27 17:03:59 +00:00
/// <inheritdoc cref="IVelopackLocator.IsPortable" />
public virtual bool IsPortable => Locator . IsPortable ;
2024-09-20 22:57:28 -06:00
/// <summary> OBSOLETE: Use <see cref="UpdatePendingRestart"/> instead. </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use UpdatePendingRestart instead.")]
public virtual bool IsUpdatePendingRestart => UpdatePendingRestart != null ;
/// <summary> Returns a VelopackAsset if there is a local update prepared that requires a call to <see cref="ApplyUpdatesAndRestart(VelopackAsset, string[])"/> to be applied. </summary>
public virtual VelopackAsset ? UpdatePendingRestart {
2023-12-22 08:41:11 +00:00
get {
2023-12-26 19:54:05 +00:00
var latestLocal = Locator . GetLatestLocalFullPackage ();
2024-01-16 16:41:59 +00:00
if ( latestLocal != null && CurrentVersion != null && latestLocal . Version > CurrentVersion )
2024-09-20 22:57:28 -06:00
return latestLocal ;
return null ;
2023-12-22 08:41:11 +00:00
}
}
2023-12-31 11:23:38 +00:00
/// <summary> The currently installed app version when you created your release. Null if this is not a currently installed app. </summary>
2024-01-16 16:41:59 +00:00
public virtual SemanticVersion ? CurrentVersion => Locator . CurrentlyInstalledVersion ;
2023-12-21 13:15:34 +00:00
2023-12-24 12:42:19 +00:00
/// <summary> The update source to use when checking for/downloading updates. </summary>
2023-12-21 13:15:34 +00:00
protected IUpdateSource Source { get ; }
2023-12-24 12:42:19 +00:00
/// <summary> The logger to use for diagnostic messages. </summary>
2023-12-21 13:15:34 +00:00
protected ILogger Log { get ; }
2023-12-24 12:42:19 +00:00
/// <summary> The locator to use when searching for local file paths. </summary>
2023-12-31 11:09:44 +00:00
protected IVelopackLocator Locator { get ; }
2023-12-21 13:15:34 +00:00
2024-01-15 11:30:43 +00:00
/// <summary> The channel to use when searching for packages. </summary>
protected string Channel { get ; }
2024-01-31 11:55:55 +00:00
/// <summary> The default channel to search for packages in, if one was not provided by the user. </summary>
protected string DefaultChannel => Locator ?. Channel ?? VelopackRuntimeInfo . SystemOs . GetOsShortName ();
/// <summary> If true, an explicit channel was provided by the user, and it's different than the default channel. </summary>
protected bool IsNonDefaultChannel => Locator ?. Channel != null && Channel != DefaultChannel ;
/// <summary> If true, UpdateManager should return the latest asset in the feed, even if that version is lower than the current version. </summary>
protected bool ShouldAllowVersionDowngrade { get ; }
2023-12-24 12:42:19 +00:00
/// <summary>
/// Creates a new UpdateManager instance using the specified URL or file path to the releases feed, and the specified channel name.
/// </summary>
/// <param name="urlOrPath">A basic URL or file path to use when checking for updates.</param>
2024-01-31 11:55:55 +00:00
/// <param name="options">Override / configure default update behaviors.</param>
2024-01-15 11:30:43 +00:00
/// <param name="logger">The logger to use for diagnostic messages. If one was provided to <see cref="VelopackApp.Run(ILogger)"/> but is null here,
/// it will be cached and used again.</param>
2023-12-31 11:09:44 +00:00
/// <param name="locator">This should usually be left null. Providing an <see cref="IVelopackLocator" /> allows you to mock up certain application paths.
/// For example, if you wanted to test that updates are working in a unit test, you could provide an instance of <see cref="TestVelopackLocator"/>. </param>
2024-01-31 11:55:55 +00:00
public UpdateManager ( string urlOrPath , UpdateOptions ? options = null , ILogger ? logger = null , IVelopackLocator ? locator = null )
: this ( CreateSimpleSource ( urlOrPath ), options , logger , locator )
2023-12-21 13:15:34 +00:00
{
}
2023-12-24 12:42:19 +00:00
/// <summary>
/// Creates a new UpdateManager instance using the specified URL or file path to the releases feed, and the specified channel name.
/// </summary>
/// <param name="source">The source describing where to search for updates. This can be a custom source, if you are integrating with some private resource,
/// or it could be one of the predefined sources. (eg. <see cref="SimpleWebSource"/> or <see cref="GithubSource"/>, etc).</param>
2024-01-31 11:55:55 +00:00
/// <param name="options">Override / configure default update behaviors.</param>
2024-01-15 11:30:43 +00:00
/// <param name="logger">The logger to use for diagnostic messages. If one was provided to <see cref="VelopackApp.Run(ILogger)"/> but is null here,
/// it will be cached and used again.</param>
2023-12-31 11:09:44 +00:00
/// <param name="locator">This should usually be left null. Providing an <see cref="IVelopackLocator" /> allows you to mock up certain application paths.
/// For example, if you wanted to test that updates are working in a unit test, you could provide an instance of <see cref="TestVelopackLocator"/>. </param>
2024-01-31 11:55:55 +00:00
public UpdateManager ( IUpdateSource source , UpdateOptions ? options = null , ILogger ? logger = null , IVelopackLocator ? locator = null )
2023-12-21 13:15:34 +00:00
{
if ( source == null ) {
throw new ArgumentNullException ( nameof ( source ));
}
2025-01-10 11:37:20 +00:00
2023-12-21 13:15:34 +00:00
Source = source ;
2024-01-15 11:30:43 +00:00
Log = logger ?? VelopackApp . DefaultLogger ?? NullLogger . Instance ;
2024-07-14 16:25:09 +01:00
Locator = locator ?? VelopackApp . DefaultLocator ?? VelopackLocator . GetDefault ( Log );
2024-01-31 11:55:55 +00:00
Channel = options ?. ExplicitChannel ?? DefaultChannel ;
ShouldAllowVersionDowngrade = options ?. AllowVersionDowngrade ?? false ;
2023-12-21 13:15:34 +00:00
}
2024-01-24 15:03:50 +00:00
/// <inheritdoc cref="CheckForUpdatesAsync()"/>
2024-01-16 16:41:59 +00:00
public UpdateInfo ? CheckForUpdates ()
2023-12-21 14:50:41 +00:00
{
return CheckForUpdatesAsync ()
. ConfigureAwait ( false ). GetAwaiter (). GetResult ();
}
2023-12-24 12:42:19 +00:00
/// <summary>
/// Checks for updates, returning null 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.
/// </summary>
/// <returns>Null if no updates, otherwise <see cref="UpdateInfo"/> containing the version of the latest update available.</returns>
2024-01-24 15:03:50 +00:00
public virtual async Task < UpdateInfo ?> CheckForUpdatesAsync ()
2023-12-21 13:15:34 +00:00
{
EnsureInstalled ();
2024-01-16 16:41:59 +00:00
var installedVer = CurrentVersion !;
2023-12-21 13:15:34 +00:00
var betaId = Locator . GetOrCreateStagedUserId ();
2023-12-26 19:54:05 +00:00
var latestLocalFull = Locator . GetLatestLocalFullPackage ();
2023-12-21 13:15:34 +00:00
Log . Debug ( "Retrieving latest release feed." );
2024-01-16 16:41:59 +00:00
var feedObj = await Source . GetReleaseFeed ( Log , Channel , betaId , latestLocalFull ). ConfigureAwait ( false );
2024-01-15 17:04:03 +00:00
var feed = feedObj . Assets ;
2023-12-21 13:15:34 +00:00
2024-09-21 12:48:05 -06:00
var latestRemoteFull = feed . Where ( r => r . Type == VelopackAssetType . Full ). MaxByPolyfill ( x => x . Version ). FirstOrDefault ();
2023-12-21 13:15:34 +00:00
if ( latestRemoteFull == null ) {
2024-01-15 17:04:03 +00:00
Log . Info ( "No remote full releases found." );
2023-12-21 13:15:34 +00:00
return null ;
}
2024-01-31 11:55:55 +00:00
// there's a newer version available, easy.
if ( latestRemoteFull . Version > installedVer ) {
Log . Info ( $"Found newer remote release available ({installedVer} -> {latestRemoteFull.Version})." );
return CreateDeltaUpdateStrategy ( feed , latestLocalFull , latestRemoteFull );
2023-12-21 13:15:34 +00:00
}
2024-01-31 11:55:55 +00:00
// if the remote version is < than current version and downgrade is enabled
if ( latestRemoteFull . Version < installedVer && ShouldAllowVersionDowngrade ) {
Log . Info ( $"Latest remote release is older than current, and downgrade is enabled ({installedVer} -> {latestRemoteFull.Version})." );
2024-01-31 18:51:05 +00:00
return new UpdateInfo ( latestRemoteFull , true );
2024-01-31 11:55:55 +00:00
}
// if the remote version is the same as current version, and downgrade is enabled,
// and we're searching for a different channel than current
if ( ShouldAllowVersionDowngrade && IsNonDefaultChannel ) {
if ( VersionComparer . Compare ( latestRemoteFull . Version , installedVer , VersionComparison . Version ) == 0 ) {
2025-01-10 11:37:20 +00:00
Log . Info (
$"Latest remote release is the same version of a different channel, and downgrade is enabled ({installedVer}: {DefaultChannel} -> {Channel})." );
2024-01-31 18:51:05 +00:00
return new UpdateInfo ( latestRemoteFull , true );
2024-01-31 11:55:55 +00:00
}
}
2025-01-10 11:37:20 +00:00
Log . Info (
$"No updates, remote version ({latestRemoteFull.Version}) is not newer than current version ({installedVer}) and / or downgrade is not enabled." );
2024-01-31 11:55:55 +00:00
return null ;
}
/// <summary>
/// Given a feed of releases, and the latest local full release, and the latest remote full release, this method will return a delta
/// update strategy to be used by <see cref="DownloadUpdatesAsync(UpdateInfo, Action{int}?, bool, CancellationToken)"/>.
/// </summary>
protected virtual UpdateInfo CreateDeltaUpdateStrategy ( VelopackAsset [] feed , VelopackAsset ? latestLocalFull , VelopackAsset latestRemoteFull )
{
if ( latestLocalFull == null ) {
// TODO: for now, we're not trying to handle the case of building delta updates on top of an installation directory,
// but we can look at this in the future. Until then, Windows (installer) is the only thing which ships with a complete .nupkg
// so in all other cases, Velopack needs to download one full release before it can start using delta's.
Log . Info ( "There is no local/base package available for this update, so delta updates will be disabled." );
2024-01-31 18:51:05 +00:00
return new UpdateInfo ( latestRemoteFull , false );
2024-01-31 11:55:55 +00:00
}
EnsureInstalled ();
var installedVer = CurrentVersion !;
2023-12-21 13:15:34 +00:00
2024-01-15 19:39:20 +00:00
var matchingRemoteDelta = feed . Where ( r => r . Type == VelopackAssetType . Delta && r . Version == latestRemoteFull . Version ). FirstOrDefault ();
2023-12-21 13:15:34 +00:00
if ( matchingRemoteDelta == null ) {
2024-01-31 11:55:55 +00:00
Log . Info ( $"Unable to find any delta matching version {latestRemoteFull.Version}, so delta updates will be disabled." );
2024-01-31 18:51:05 +00:00
return new UpdateInfo ( latestRemoteFull , false );
2023-12-21 13:15:34 +00:00
}
// if we have a local full release, we try to apply delta's from that version to target version.
2024-01-31 11:55:55 +00:00
SemanticVersion deltaFromVer = latestLocalFull . Version ;
2023-12-21 13:15:34 +00:00
2024-01-15 19:39:20 +00:00
var deltas = feed . Where ( r => r . Type == VelopackAssetType . Delta && r . Version > deltaFromVer && r . Version <= latestRemoteFull . Version ). ToArray ();
2024-05-14 02:38:34 -04:00
Log . Debug ( $"Found {deltas.Length} delta release(s) between {deltaFromVer} and {latestRemoteFull.Version}." );
2024-01-31 18:51:05 +00:00
return new UpdateInfo ( latestRemoteFull , false , latestLocalFull , deltas );
2023-12-21 13:15:34 +00:00
}
2023-12-24 12:42:19 +00:00
/// <inheritdoc cref="DownloadUpdatesAsync(UpdateInfo, Action{int}, bool, CancellationToken)"/>
2024-01-16 16:41:59 +00:00
public void DownloadUpdates ( UpdateInfo updates , Action < int >? progress = null , bool ignoreDeltas = false )
2023-12-21 14:50:41 +00:00
{
2023-12-23 15:08:47 +00:00
DownloadUpdatesAsync ( updates , progress , ignoreDeltas )
2023-12-21 14:50:41 +00:00
. ConfigureAwait ( false ). GetAwaiter (). GetResult ();
}
2023-12-24 12:42:19 +00:00
/// <summary>
/// Downloads the specified updates to the local app packages directory. If the update contains delta packages and ignoreDeltas=false,
/// this method will attempt to unpack and prepare them. If there is no delta update available, or there is an error preparing delta
/// packages, this method will fall back to downloading the full version of the update. This function will acquire a global update lock
/// so may fail if there is already another update operation in progress.
/// </summary>
/// <param name="updates">The updates to download. Should be retrieved from <see cref="CheckForUpdates"/>.</param>
/// <param name="progress">The progress callback. Will be called with values from 0-100.</param>
/// <param name="ignoreDeltas">Whether to attempt downloading delta's or skip to full package download.</param>
/// <param name="cancelToken">An optional cancellation token if you wish to stop this operation.</param>
2023-12-23 15:08:47 +00:00
public virtual async Task DownloadUpdatesAsync (
2024-01-16 16:41:59 +00:00
UpdateInfo updates , Action < int >? progress = null , bool ignoreDeltas = false , CancellationToken cancelToken = default )
2023-12-21 13:15:34 +00:00
{
2024-01-01 13:03:44 +00:00
progress ??= ( _ => { });
2024-01-03 19:01:30 +00:00
// the progress delegate may very likely invoke into the client main thread for UI updates, so
// let's try to reduce the spam. report only on even numbers and only if the progress has changed.
int lastProgress = 0 ;
2025-01-10 11:37:20 +00:00
2024-01-03 19:01:30 +00:00
void reportProgress ( int x )
{
int result = ( int ) ( Math . Round ( x / 2d , MidpointRounding . AwayFromZero ) * 2d );
if ( result != lastProgress ) {
lastProgress = result ;
progress ( result );
}
}
2024-01-16 16:41:59 +00:00
if ( updates == null ) {
throw new ArgumentNullException ( nameof ( updates ));
}
var targetRelease = updates . TargetFullRelease ;
2024-01-01 13:03:44 +00:00
if ( targetRelease == null ) {
throw new ArgumentException ( "Must pass a valid UpdateInfo object with a non-null TargetFullRelease" , nameof ( updates ));
}
EnsureInstalled ();
2024-11-07 20:42:28 +00:00
using var _mut = await AcquireUpdateLock (). ConfigureAwait ( false );
2024-01-01 13:03:44 +00:00
2024-01-16 16:41:59 +00:00
var appTempDir = Locator . AppTempDir !;
2024-06-20 11:13:46 +02:00
var completeFile = Locator . GetLocalPackagePath ( targetRelease );
2024-01-01 13:03:44 +00:00
var incompleteFile = completeFile + ".partial" ;
2024-01-31 19:11:51 +00:00
try {
2024-01-31 19:26:02 +00:00
// if the package already exists on disk, we can skip the download.
if ( File . Exists ( completeFile )) {
Log . Info ( $"Package already exists on disk: '{completeFile}', verifying checksum..." );
try {
2024-06-20 11:13:46 +02:00
VerifyPackageChecksum ( targetRelease , completeFile );
2024-01-31 19:26:02 +00:00
Log . Info ( "Package checksum verified, skipping download." );
return ;
} catch ( ChecksumFailedException ex ) {
Log . Warn ( ex , $"Checksum failed for file '{completeFile}'. Deleting and starting over." );
}
}
2024-01-15 17:04:03 +00:00
var deltasSize = updates . DeltasToTarget . Sum ( x => x . Size );
2023-12-21 16:25:48 +00:00
var deltasCount = updates . DeltasToTarget . Count ();
try {
2024-01-16 16:41:59 +00:00
if ( updates . BaseRelease ?. FileName != null && deltasCount > 0 ) {
2023-12-21 16:25:48 +00:00
if ( ignoreDeltas ) {
Log . Info ( "Ignoring delta updates (ignoreDeltas parameter)" );
} else {
2024-01-15 17:04:03 +00:00
if ( deltasCount > 10 || deltasSize > targetRelease . Size ) {
2025-01-10 11:37:20 +00:00
Log . Info (
$"There are too many delta's ({deltasCount} > 10) or the sum of their size ({deltasSize} > {targetRelease.Size}) is too large. " +
2023-12-21 16:25:48 +00:00
$"Only full update will be available." );
} else {
2024-09-21 12:48:05 -06:00
using var _1 = TempUtil . GetTempDirectory ( out var deltaStagingDir , appTempDir );
2024-06-20 11:13:46 +02:00
string basePackagePath = Locator . GetLocalPackagePath ( updates . BaseRelease );
2024-01-16 16:41:59 +00:00
if (! File . Exists ( basePackagePath ))
throw new Exception ( $"Unable to find base package {basePackagePath} for delta update." );
EasyZip . ExtractZipToDirectory ( Log , basePackagePath , deltaStagingDir );
2024-01-03 19:01:30 +00:00
reportProgress ( 10 );
2025-01-10 11:37:20 +00:00
await DownloadAndApplyDeltaUpdates (
deltaStagingDir ,
updates ,
x => reportProgress ( CoreUtil . CalculateProgress ( x , 10 , 80 )),
cancelToken )
2023-12-21 16:25:48 +00:00
. ConfigureAwait ( false );
2024-01-03 19:01:30 +00:00
reportProgress ( 80 );
2023-12-21 16:25:48 +00:00
Log . Info ( "Delta updates completed, creating final update package." );
File . Delete ( incompleteFile );
2025-01-10 11:37:20 +00:00
await EasyZip . CreateZipFromDirectoryAsync (
Log ,
incompleteFile ,
deltaStagingDir ,
x => reportProgress ( CoreUtil . CalculateProgress ( x , 80 , 100 )),
2024-01-24 15:03:50 +00:00
cancelToken : cancelToken ). ConfigureAwait ( false );
2023-12-21 16:25:48 +00:00
File . Delete ( completeFile );
File . Move ( incompleteFile , completeFile );
Log . Info ( "Delta release preparations complete. Package moved to: " + completeFile );
2024-01-03 19:01:30 +00:00
reportProgress ( 100 );
2023-12-21 16:25:48 +00:00
return ; // success!
}
}
}
2024-01-31 18:51:05 +00:00
} catch ( Exception ex ) when (! VelopackRuntimeInfo . InUnitTestRunner ) {
2023-12-21 16:25:48 +00:00
Log . Warn ( ex , "Unable to apply delta updates, falling back to full update." );
}
2024-01-15 17:04:03 +00:00
Log . Info ( $"Downloading full release ({targetRelease.FileName})" );
2023-12-21 16:25:48 +00:00
File . Delete ( incompleteFile );
2024-01-24 15:03:50 +00:00
await Source . DownloadReleaseEntry ( Log , targetRelease , incompleteFile , reportProgress , cancelToken ). ConfigureAwait ( false );
2023-12-21 16:25:48 +00:00
Log . Info ( "Verifying package checksum..." );
VerifyPackageChecksum ( targetRelease , incompleteFile );
2024-01-31 18:51:05 +00:00
2024-09-21 12:48:05 -06:00
IoUtil . MoveFile ( incompleteFile , completeFile , true );
2023-12-21 16:25:48 +00:00
Log . Info ( "Full release download complete. Package moved to: " + completeFile );
2024-01-03 19:01:30 +00:00
reportProgress ( 100 );
2023-12-21 16:25:48 +00:00
} finally {
2024-01-24 15:03:50 +00:00
if ( VelopackRuntimeInfo . IsWindows && ! cancelToken . IsCancellationRequested ) {
2024-01-01 13:03:44 +00:00
try {
2024-01-16 16:41:59 +00:00
var updateExe = Locator . UpdateExePath !;
2024-01-01 13:03:44 +00:00
Log . Info ( "Extracting new Update.exe to " + updateExe );
2024-01-21 11:15:24 +00:00
var zip = new ZipPackage ( completeFile , loadUpdateExe : true );
2024-01-01 13:03:44 +00:00
if ( zip . UpdateExeBytes == null ) {
Log . Error ( "Update.exe not found in package, skipping extraction." );
} else {
2025-01-10 11:37:20 +00:00
await IoUtil . RetryAsync (
async () => {
using var ms = new MemoryStream ( zip . UpdateExeBytes );
using var fs = File . Create ( updateExe );
await ms . CopyToAsync ( fs ). ConfigureAwait ( false );
}). ConfigureAwait ( false );
2024-01-01 13:03:44 +00:00
}
} catch ( Exception ex ) {
Log . Error ( ex , "Failed to extract new Update.exe" );
}
}
2024-01-31 19:26:02 +00:00
CleanPackagesExcept ( completeFile );
2023-12-21 16:25:48 +00:00
}
2023-12-21 13:15:34 +00:00
}
2023-12-24 12:42:19 +00:00
/// <summary>
2024-01-16 16:41:59 +00:00
/// Given a folder containing the extracted base package, and a list of delta updates, downloads and applies the
/// delta updates to the base package.
2023-12-24 12:42:19 +00:00
/// </summary>
/// <param name="extractedBasePackage">A folder containing the application files to apply the delta's to.</param>
/// <param name="updates">An update object containing one or more delta's</param>
/// <param name="progress">A callback reporting process of delta application progress (from 0-100).</param>
2024-01-24 15:03:50 +00:00
/// <param name="cancelToken">A token to use to cancel the request.</param>
2025-01-10 11:37:20 +00:00
protected virtual async Task DownloadAndApplyDeltaUpdates ( string extractedBasePackage , UpdateInfo updates , Action < int > progress ,
CancellationToken cancelToken )
2023-12-21 13:15:34 +00:00
{
var releasesToDownload = updates . DeltasToTarget . OrderBy ( d => d . Version ). ToArray ();
2024-01-16 16:41:59 +00:00
var appTempDir = Locator . AppTempDir !;
var updateExe = Locator . UpdateExePath !;
2023-12-21 13:15:34 +00:00
// downloading accounts for 0%-50% of progress
double current = 0 ;
double toIncrement = 100.0 / releasesToDownload . Count ();
2025-01-10 11:37:20 +00:00
await releasesToDownload . ForEachAsync (
async x => {
var targetFile = Locator . GetLocalPackagePath ( x );
double component = 0 ;
Log . Debug ( $"Downloading delta version {x.Version}" );
await Source . DownloadReleaseEntry (
Log ,
x ,
targetFile ,
p => {
lock ( progress ) {
current -= component ;
component = toIncrement / 100.0 * p ;
var progressOfStep = ( int ) Math . Round ( current += component );
progress ( CoreUtil . CalculateProgress ( progressOfStep , 0 , 50 ));
}
},
cancelToken ). ConfigureAwait ( false );
VerifyPackageChecksum ( x , targetFile );
cancelToken . ThrowIfCancellationRequested ();
Log . Debug ( $"Download complete for delta version {x.Version}" );
}). ConfigureAwait ( false );
2023-12-21 13:15:34 +00:00
Log . Info ( "All delta packages downloaded and verified, applying them to the base now. The delta staging dir is: " + extractedBasePackage );
// applying deltas accounts for 50%-100% of progress
double progressStepSize = 100d / releasesToDownload . Length ;
2024-01-16 16:41:59 +00:00
var builder = new DeltaUpdateExe ( Log , appTempDir , updateExe );
2023-12-21 13:15:34 +00:00
for ( var i = 0 ; i < releasesToDownload . Length ; i ++) {
2024-01-24 15:03:50 +00:00
cancelToken . ThrowIfCancellationRequested ();
2023-12-21 13:15:34 +00:00
var rel = releasesToDownload [ i ];
double baseProgress = i * progressStepSize ;
2024-06-20 11:13:46 +02:00
var packageFile = Locator . GetLocalPackagePath ( rel );
2025-01-10 11:37:20 +00:00
builder . ApplyDeltaPackageFast (
extractedBasePackage ,
packageFile ,
x => {
var progressOfStep = ( int ) ( baseProgress + ( progressStepSize * ( x / 100d )));
progress ( CoreUtil . CalculateProgress ( progressOfStep , 50 , 100 ));
});
2023-12-21 13:15:34 +00:00
}
progress ( 100 );
}
2023-12-24 12:42:19 +00:00
/// <summary>
2024-01-31 19:26:02 +00:00
/// Removes any incomplete files (.partial) and packages (.nupkg) from the packages directory that does not match
/// the provided asset. If assetToKeep is null, all packages will be deleted.
2023-12-24 12:42:19 +00:00
/// </summary>
2024-01-31 19:26:02 +00:00
protected virtual void CleanPackagesExcept ( string? assetToKeep )
2023-12-21 16:25:48 +00:00
{
try {
Log . Info ( "Cleaning up incomplete and delta packages from packages directory." );
2024-01-31 19:26:02 +00:00
var appPackageDir = Locator . PackagesDir !;
foreach ( var l in Directory . EnumerateFiles ( appPackageDir , "*.nupkg" ). ToArray ()) {
try {
2024-09-21 12:48:05 -06:00
if ( assetToKeep != null && PathUtil . FullPathEquals ( l , assetToKeep )) {
2024-01-31 19:26:02 +00:00
continue ;
2023-12-21 16:25:48 +00:00
}
2024-01-31 19:26:02 +00:00
2024-09-21 12:48:05 -06:00
IoUtil . DeleteFileOrDirectoryHard ( l );
2024-01-31 19:26:02 +00:00
Log . Trace ( l + " deleted." );
} catch ( Exception ex ) {
Log . Warn ( ex , "Failed to delete partial package: " + l );
2023-12-21 16:25:48 +00:00
}
}
2024-01-16 16:41:59 +00:00
foreach ( var l in Directory . EnumerateFiles ( appPackageDir , "*.partial" ). ToArray ()) {
2023-12-21 16:25:48 +00:00
try {
2024-09-21 12:48:05 -06:00
IoUtil . DeleteFileOrDirectoryHard ( l );
2023-12-21 16:25:48 +00:00
Log . Trace ( l + " deleted." );
} catch ( Exception ex ) {
Log . Warn ( ex , "Failed to delete partial package: " + l );
}
}
} catch ( Exception ex ) {
Log . Warn ( ex , "Failed to clean up incomplete and delta packages." );
}
}
2023-12-24 12:42:19 +00:00
/// <summary>
/// Check a package checksum against the one in the release entry, and throws if the checksum does not match.
/// </summary>
/// <param name="release">The entry to check</param>
/// <param name="filePathOverride">Optional file path, if not specified the package will be loaded from %pkgdir%/release.OriginalFilename.</param>
2024-01-16 16:41:59 +00:00
protected internal virtual void VerifyPackageChecksum ( VelopackAsset release , string? filePathOverride = null )
2023-12-21 13:15:34 +00:00
{
2024-06-20 11:13:46 +02:00
var targetPackage = new FileInfo ( filePathOverride ?? Locator . GetLocalPackagePath ( release ));
2023-12-21 13:15:34 +00:00
if (! targetPackage . Exists ) {
throw new ChecksumFailedException ( targetPackage . FullName , "File doesn't exist." );
}
2024-01-15 17:04:03 +00:00
if ( targetPackage . Length != release . Size ) {
throw new ChecksumFailedException ( targetPackage . FullName , $"Size doesn't match ({targetPackage.Length} != {release.Size})." );
2023-12-21 13:15:34 +00:00
}
2024-06-20 11:13:00 +02:00
if (! string . IsNullOrEmpty ( release . SHA256 )) {
2024-09-21 12:48:05 -06:00
var hash = IoUtil . CalculateFileSHA256 ( targetPackage . FullName );
2024-06-20 11:13:00 +02:00
if (! hash . Equals ( release . SHA256 , StringComparison . Ordinal )) {
2024-06-20 10:01:50 +02:00
throw new ChecksumFailedException ( targetPackage . FullName , $"SHA256 doesn't match ({release.SHA256} != {hash})." );
}
} else {
2024-09-21 12:48:05 -06:00
var hash = IoUtil . CalculateFileSHA1 ( targetPackage . FullName );
2024-06-20 10:01:50 +02:00
if (! hash . Equals ( release . SHA1 , StringComparison . OrdinalIgnoreCase )) {
throw new ChecksumFailedException ( targetPackage . FullName , $"SHA1 doesn't match ({release.SHA1} != {hash})." );
}
2025-01-10 11:37:20 +00:00
}
2023-12-21 13:15:34 +00:00
}
2023-12-24 12:42:19 +00:00
/// <summary>
/// Throws an exception if the current application is not installed.
/// </summary>
2023-12-21 13:15:34 +00:00
protected virtual void EnsureInstalled ()
{
if ( AppId == null || ! IsInstalled )
2025-01-10 11:37:20 +00:00
throw new NotInstalledException ();
2023-12-21 13:15:34 +00:00
}
2023-12-24 12:42:19 +00:00
/// <summary>
/// Acquires a globally unique mutex/lock for the current application, to avoid concurrent install/uninstall/update operations.
/// </summary>
2024-11-02 17:42:28 +00:00
protected virtual async Task < IDisposable > AcquireUpdateLock ()
2023-12-21 13:15:34 +00:00
{
2024-11-02 17:42:28 +00:00
var dir = Directory . CreateDirectory ( Locator . PackagesDir !);
var lockPath = Path . Combine ( dir . FullName , ".velopack_lock" );
2024-11-03 10:38:00 +00:00
var fsLock = new LockFile ( lockPath );
2024-11-02 17:42:28 +00:00
await fsLock . LockAsync (). ConfigureAwait ( false );
return fsLock ;
2023-12-21 13:15:34 +00:00
}
2024-01-15 11:30:43 +00:00
private static IUpdateSource CreateSimpleSource ( string urlOrPath )
2023-12-21 13:15:34 +00:00
{
if ( String . IsNullOrWhiteSpace ( urlOrPath )) {
throw new ArgumentException ( "Must pass a valid URL or file path to UpdateManager" , nameof ( urlOrPath ));
}
2025-01-10 11:37:20 +00:00
2024-09-21 12:48:05 -06:00
if ( HttpUtil . IsHttpUrl ( urlOrPath )) {
return new SimpleWebSource ( urlOrPath , HttpUtil . CreateDefaultDownloader ());
2023-12-21 13:15:34 +00:00
} else {
2024-01-15 11:30:43 +00:00
return new SimpleFileSource ( new DirectoryInfo ( urlOrPath ));
2023-12-21 13:15:34 +00:00
}
}
}
2025-01-10 11:37:20 +00:00
}