diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dc3c2a6..b15744a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,7 @@ jobs: main: uses: Tyrrrz/.github/.github/workflows/nuget.yml@master with: + windows-only: true deploy: ${{ inputs.deploy || github.ref_type == 'tag' }} package-version: ${{ inputs.package-version || (github.ref_type == 'tag' && github.ref_name) || format('0.0.0-ci-{0}', github.sha) }} dotnet-version: 8.0.x diff --git a/Onova.Tests.Dummy/Onova.Tests.Dummy.csproj b/Onova.Tests.Dummy/Onova.Tests.Dummy.csproj index 36736c8..2f4dcf8 100644 --- a/Onova.Tests.Dummy/Onova.Tests.Dummy.csproj +++ b/Onova.Tests.Dummy/Onova.Tests.Dummy.csproj @@ -2,11 +2,11 @@ Exe - net8.0 + net8.0-windows - + diff --git a/Onova.Tests/Onova.Tests.csproj b/Onova.Tests/Onova.Tests.csproj index 81c4d05..fd77e25 100644 --- a/Onova.Tests/Onova.Tests.csproj +++ b/Onova.Tests/Onova.Tests.csproj @@ -1,19 +1,23 @@  - net8.0 + net8.0-windows - - - + + + + + + + - + - - + + @@ -21,8 +25,4 @@ - - - - \ No newline at end of file diff --git a/Onova.Tests/UpdateSpecs.cs b/Onova.Tests/UpdateSpecs.cs index 1aaf954..e57ed51 100644 --- a/Onova.Tests/UpdateSpecs.cs +++ b/Onova.Tests/UpdateSpecs.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using FluentAssertions; @@ -45,7 +46,11 @@ public void Dispose() public async Task I_can_check_for_updates_and_get_a_higher_version_if_it_is_available() { // Arrange - var updatee = new AssemblyMetadata("TestUpdatee", Version.Parse("1.0"), ""); + var updatee = new AssemblyMetadata( + "TestUpdatee", + Version.Parse("1.0"), + Assembly.GetExecutingAssembly().Location + ); // Cleanup storage directory (TODO: move this to API) DirectoryEx.DeleteIfExists( @@ -82,7 +87,11 @@ public async Task I_can_check_for_updates_and_get_a_higher_version_if_it_is_avai public async Task I_can_check_for_updates_and_get_nothing_if_there_is_no_higher_version_available() { // Arrange - var updatee = new AssemblyMetadata("TestUpdatee", Version.Parse("3.0"), ""); + var updatee = new AssemblyMetadata( + "TestUpdatee", + Version.Parse("3.0"), + Assembly.GetExecutingAssembly().Location + ); // Cleanup storage directory (TODO: move this to API) DirectoryEx.DeleteIfExists( @@ -119,7 +128,11 @@ public async Task I_can_check_for_updates_and_get_nothing_if_there_is_no_higher_ public async Task I_can_check_for_updates_and_get_nothing_if_the_package_source_contains_no_packages() { // Arrange - var updatee = new AssemblyMetadata("TestUpdatee", Version.Parse("1.0"), ""); + var updatee = new AssemblyMetadata( + "TestUpdatee", + Version.Parse("1.0"), + Assembly.GetExecutingAssembly().Location + ); // Cleanup storage directory (TODO: move this to API) DirectoryEx.DeleteIfExists( @@ -151,7 +164,11 @@ public async Task I_can_check_for_updates_and_get_nothing_if_the_package_source_ public async Task I_can_prepare_an_update_so_that_it_can_be_installed() { // Arrange - var updatee = new AssemblyMetadata("TestUpdatee", Version.Parse("1.0"), ""); + var updatee = new AssemblyMetadata( + "TestUpdatee", + Version.Parse("1.0"), + Assembly.GetExecutingAssembly().Location + ); // Cleanup storage directory (TODO: move this to API) DirectoryEx.DeleteIfExists( @@ -188,7 +205,11 @@ public async Task I_can_prepare_an_update_so_that_it_can_be_installed() public async Task I_can_get_a_list_of_updates_which_are_already_prepared_to_install() { // Arrange - var updatee = new AssemblyMetadata("TestUpdatee", Version.Parse("1.0"), ""); + var updatee = new AssemblyMetadata( + "TestUpdatee", + Version.Parse("1.0"), + Assembly.GetExecutingAssembly().Location + ); // Cleanup storage directory (TODO: move this to API) DirectoryEx.DeleteIfExists( diff --git a/Onova.Tests/Utils/DummyEnvironment.cs b/Onova.Tests/Utils/DummyEnvironment.cs index 86df8bc..696b145 100644 --- a/Onova.Tests/Utils/DummyEnvironment.cs +++ b/Onova.Tests/Utils/DummyEnvironment.cs @@ -11,7 +11,7 @@ namespace Onova.Tests.Utils; -internal class DummyEnvironment : IDisposable +internal class DummyEnvironment(string rootDirPath) : IDisposable { private static readonly Assembly DummyAssembly = typeof(Dummy.Program).Assembly; private static readonly string DummyAssemblyFileName = Path.GetFileName(DummyAssembly.Location); @@ -19,19 +19,9 @@ internal class DummyEnvironment : IDisposable DummyAssembly.Location )!; - private readonly string _rootDirPath; + private string DummyFilePath { get; } = Path.Combine(rootDirPath, DummyAssemblyFileName); - private string DummyFilePath { get; } - - private string DummyPackagesDirPath { get; } - - public DummyEnvironment(string rootDirPath) - { - _rootDirPath = rootDirPath; - - DummyFilePath = Path.Combine(_rootDirPath, DummyAssemblyFileName); - DummyPackagesDirPath = Path.Combine(_rootDirPath, "Packages"); - } + private string DummyPackagesDirPath { get; } = Path.Combine(rootDirPath, "Packages"); private void SetAssemblyVersion(string filePath, Version version) { @@ -45,13 +35,13 @@ private void SetAssemblyVersion(string filePath, Version version) private void CreateBase(Version version) { // Create dummy directory - Directory.CreateDirectory(_rootDirPath); + Directory.CreateDirectory(rootDirPath); // Copy files foreach (var filePath in Directory.EnumerateFiles(DummyAssemblyDirPath)) { var fileName = Path.GetFileName(filePath); - File.Copy(filePath, Path.Combine(_rootDirPath, fileName)); + File.Copy(filePath, Path.Combine(rootDirPath, fileName)); } // Change base dummy version @@ -64,7 +54,7 @@ private void CreatePackage(Version version) Directory.CreateDirectory(DummyPackagesDirPath); // Temporarily copy the dummy - var dummyTempFilePath = Path.Combine(_rootDirPath, $"{DummyAssemblyFileName}.{version}"); + var dummyTempFilePath = Path.Combine(rootDirPath, $"{DummyAssemblyFileName}.{version}"); File.Copy(DummyFilePath, dummyTempFilePath); // Change dummy version @@ -91,7 +81,7 @@ private void Cleanup() { try { - DirectoryEx.DeleteIfExists(_rootDirPath); + DirectoryEx.DeleteIfExists(rootDirPath); break; } catch (UnauthorizedAccessException) when (retriesRemaining > 0) @@ -113,7 +103,7 @@ public void Setup(Version baseVersion, IReadOnlyList availableVersions) public string[] GetLastRunArguments(Version version) { - var filePath = Path.Combine(_rootDirPath, $"lastrun-{version}.txt"); + var filePath = Path.Combine(rootDirPath, $"lastrun-{version}.txt"); return File.Exists(filePath) ? File.ReadAllLines(filePath) : Array.Empty(); } diff --git a/Onova.Updater/Onova.Updater.csproj b/Onova.Updater/Onova.Updater.csproj index bd1820a..4b47952 100644 --- a/Onova.Updater/Onova.Updater.csproj +++ b/Onova.Updater/Onova.Updater.csproj @@ -7,8 +7,8 @@ - - + + \ No newline at end of file diff --git a/Onova.Updater/Updater.cs b/Onova.Updater/Updater.cs index f2b05fb..5fc9159 100644 --- a/Onova.Updater/Updater.cs +++ b/Onova.Updater/Updater.cs @@ -8,35 +8,23 @@ namespace Onova.Updater; -public class Updater : IDisposable +public class Updater( + string updateeFilePath, + string packageContentDirPath, + bool restartUpdatee, + string routedArgs +) : IDisposable { - private readonly string _updateeFilePath; - private readonly string _packageContentDirPath; - private readonly bool _restartUpdatee; - private readonly string _routedArgs; - private readonly TextWriter _log = File.CreateText( Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log.txt") ); - public Updater( - string updateeFilePath, - string packageContentDirPath, - bool restartUpdatee, - string routedArgs - ) - { - _updateeFilePath = updateeFilePath; - _packageContentDirPath = packageContentDirPath; - _restartUpdatee = restartUpdatee; - _routedArgs = routedArgs; - } - private void WriteLog(string content) { - var date = DateTimeOffset - .Now - .ToString("dd-MMM-yyyy HH:mm:ss.fff", CultureInfo.InvariantCulture); + var date = DateTimeOffset.Now.ToString( + "dd-MMM-yyyy HH:mm:ss.fff", + CultureInfo.InvariantCulture + ); var entry = $"{date}> {content}"; @@ -51,7 +39,7 @@ private void WaitForUpdateeExit() for (var retriesRemaining = 15; retriesRemaining > 0; retriesRemaining--) { - if (FileEx.CheckWriteAccess(_updateeFilePath)) + if (FileEx.CheckWriteAccess(updateeFilePath)) return; Thread.Sleep(1000); @@ -63,12 +51,12 @@ private void WaitForUpdateeExit() private void ApplyUpdate() { WriteLog("Copying package contents from storage to the updatee directory..."); - DirectoryEx.Copy(_packageContentDirPath, Path.GetDirectoryName(_updateeFilePath)!); + DirectoryEx.Copy(packageContentDirPath, Path.GetDirectoryName(updateeFilePath)!); try { WriteLog("Deleting package contents from storage..."); - Directory.Delete(_packageContentDirPath, true); + Directory.Delete(packageContentDirPath, true); } catch (Exception ex) { @@ -79,40 +67,38 @@ private void ApplyUpdate() private void StartUpdatee() { - using var process = new Process + using var process = new Process(); + process.StartInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - WorkingDirectory = Path.GetDirectoryName(_updateeFilePath), - Arguments = _routedArgs, - // Don't let the child process inherit the current console window - UseShellExecute = true - } + WorkingDirectory = Path.GetDirectoryName(updateeFilePath), + Arguments = routedArgs, + // Don't let the child process inherit the current console window + UseShellExecute = true }; // If the updatee is an .exe file, start it directly. // This covers self-contained .NET Core apps and legacy .NET Framework apps. if ( string.Equals( - Path.GetExtension(_updateeFilePath), + Path.GetExtension(updateeFilePath), ".exe", StringComparison.OrdinalIgnoreCase ) ) { - process.StartInfo.FileName = _updateeFilePath; + process.StartInfo.FileName = updateeFilePath; } // Otherwise, locate the apphost by looking for the .exe file with the same name. // This covers framework-dependent .NET Core apps. - else if (File.Exists(Path.ChangeExtension(_updateeFilePath, ".exe"))) + else if (File.Exists(Path.ChangeExtension(updateeFilePath, ".exe"))) { - process.StartInfo.FileName = Path.ChangeExtension(_updateeFilePath, ".exe"); + process.StartInfo.FileName = Path.ChangeExtension(updateeFilePath, ".exe"); } // As a fallback, try to run the updatee through the .NET CLI else { process.StartInfo.FileName = "dotnet"; - process.StartInfo.Arguments = $"{_updateeFilePath} {_routedArgs}"; + process.StartInfo.Arguments = $"{updateeFilePath} {routedArgs}"; } WriteLog( @@ -131,17 +117,17 @@ public void Run() WriteLog( $""" Onova Updater v{updaterVersion} started with the following arguments: - - UpdateeFilePath = {_updateeFilePath} - - PackageContentDirPath = {_packageContentDirPath} - - RestartUpdatee = {_restartUpdatee} - - RoutedArgs = {_routedArgs} + - UpdateeFilePath = {updateeFilePath} + - PackageContentDirPath = {packageContentDirPath} + - RestartUpdatee = {restartUpdatee} + - RoutedArgs = {routedArgs} """ ); WaitForUpdateeExit(); ApplyUpdate(); - if (_restartUpdatee) + if (restartUpdatee) StartUpdatee(); WriteLog("Update completed successfully."); diff --git a/Onova/Exceptions/LockFileNotAcquiredException.cs b/Onova/Exceptions/LockFileNotAcquiredException.cs index 9494181..7e6557f 100644 --- a/Onova/Exceptions/LockFileNotAcquiredException.cs +++ b/Onova/Exceptions/LockFileNotAcquiredException.cs @@ -5,13 +5,7 @@ namespace Onova.Exceptions; /// /// Thrown when an attempt to acquire a lock file failed. /// -public class LockFileNotAcquiredException : Exception -{ - /// - /// Initializes an instance of . - /// - public LockFileNotAcquiredException() - : base( - "Could not acquire a lock file. Most likely, another instance of this application currently owns the lock file." - ) { } -} +public class LockFileNotAcquiredException() + : Exception( + "Could not acquire a lock file. Most likely, another instance of this application currently owns the lock file." + ); diff --git a/Onova/Exceptions/PackageNotFoundException.cs b/Onova/Exceptions/PackageNotFoundException.cs index e6a1d56..224e785 100644 --- a/Onova/Exceptions/PackageNotFoundException.cs +++ b/Onova/Exceptions/PackageNotFoundException.cs @@ -5,19 +5,11 @@ namespace Onova.Exceptions; /// /// Thrown when a package of given version was not found by a resolver. /// -public class PackageNotFoundException : Exception +public class PackageNotFoundException(Version version) + : Exception($"Package version '{version}' was not found by the configured package resolver.") { /// /// Package version. /// - public Version Version { get; } - - /// - /// Initializes an instance of . - /// - public PackageNotFoundException(Version version) - : base($"Package version '{version}' was not found by the configured package resolver.") - { - Version = version; - } + public Version Version { get; } = version; } diff --git a/Onova/Exceptions/UpdateNotPreparedException.cs b/Onova/Exceptions/UpdateNotPreparedException.cs index 983fa9c..18bb6b0 100644 --- a/Onova/Exceptions/UpdateNotPreparedException.cs +++ b/Onova/Exceptions/UpdateNotPreparedException.cs @@ -5,21 +5,13 @@ namespace Onova.Exceptions; /// /// Thrown when launching the updater to install an update that was not prepared. /// -public class UpdateNotPreparedException : Exception +public class UpdateNotPreparedException(Version version) + : Exception( + $"Update to version '{version}' is not prepared. Please prepare an update before applying it." + ) { /// /// Package version. /// - public Version Version { get; } - - /// - /// Initializes an instance of . - /// - public UpdateNotPreparedException(Version version) - : base( - $"Update to version '{version}' is not prepared. Please prepare an update before applying it." - ) - { - Version = version; - } + public Version Version { get; } = version; } diff --git a/Onova/Exceptions/UpdaterAlreadyLaunchedException.cs b/Onova/Exceptions/UpdaterAlreadyLaunchedException.cs index 07f0f56..150b637 100644 --- a/Onova/Exceptions/UpdaterAlreadyLaunchedException.cs +++ b/Onova/Exceptions/UpdaterAlreadyLaunchedException.cs @@ -5,13 +5,7 @@ namespace Onova.Exceptions; /// /// Thrown when launching the updater after it has already been launched. /// -public class UpdaterAlreadyLaunchedException : Exception -{ - /// - /// Initializes an instance of . - /// - public UpdaterAlreadyLaunchedException() - : base( - "Updater has already been launched, either by this or another instance of the application." - ) { } -} +public class UpdaterAlreadyLaunchedException() + : Exception( + "Updater has already been launched, either by this or another instance of the application." + ); diff --git a/Onova/Models/AssemblyMetadata.cs b/Onova/Models/AssemblyMetadata.cs index 1e24d83..c1626fc 100644 --- a/Onova/Models/AssemblyMetadata.cs +++ b/Onova/Models/AssemblyMetadata.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; @@ -8,48 +9,47 @@ namespace Onova.Models; /// /// Contains information about an assembly. /// -public partial class AssemblyMetadata +public partial class AssemblyMetadata(string name, Version version, string filePath) { /// /// Assembly name. /// - public string Name { get; } + public string Name { get; } = name; /// /// Assembly version. /// - public Version Version { get; } + public Version Version { get; } = version; /// /// Assembly file path. /// - public string FilePath { get; } - - internal string DirPath => Path.GetDirectoryName(FilePath)!; + public string FilePath { get; } = Path.GetFullPath(filePath); - /// - /// Initializes a new instance of . - /// - public AssemblyMetadata(string name, Version version, string filePath) - { - Name = name; - Version = version; - FilePath = filePath; - } + internal string DirPath { get; } = Path.GetDirectoryName(filePath)!; } public partial class AssemblyMetadata { /// - /// Extracts assembly metadata from given assembly. - /// The specified path is used to override the executable file path in case the assembly is not meant to run directly. + /// Extracts assembly metadata from the specified assembly. + /// The specified path is used to override the executable file path in case the assembly is not available on disk. /// public static AssemblyMetadata FromAssembly(Assembly assembly, string assemblyFilePath) => - new(assembly.GetName().Name!, assembly.GetName().Version!, assemblyFilePath); + new( + assembly.GetName().Name + ?? throw new InvalidOperationException("Provided assembly's name is ."), + assembly.GetName().Version + ?? throw new InvalidOperationException("Provided assembly's version is ."), + assemblyFilePath + ); /// - /// Extracts assembly metadata from given assembly. + /// Extracts assembly metadata from the specified assembly. /// + [RequiresAssemblyFiles( + "This method requires the specified assembly's file path to be available." + )] public static AssemblyMetadata FromAssembly(Assembly assembly) { if (string.IsNullOrEmpty(assembly.Location)) @@ -64,22 +64,30 @@ public static AssemblyMetadata FromAssembly(Assembly assembly) } /// - /// Extracts assembly metadata from entry assembly. + /// Extracts assembly metadata from the entry assembly. /// + [UnconditionalSuppressMessage( + "SingleFile", + "IL3000:Avoid accessing Assembly file path when publishing as a single file", + Justification = "The return value of the method is checked to ensure the assembly location is available." + )] public static AssemblyMetadata FromEntryAssembly() { // For most applications, the entry assembly is the entry point var assembly = Assembly.GetEntryAssembly() - ?? throw new InvalidOperationException("Can't get entry assembly."); + ?? throw new InvalidOperationException("Failed to get the entry assembly."); if (!string.IsNullOrWhiteSpace(assembly.Location)) return FromAssembly(assembly, assembly.Location); - // For self-contained applications, the executable is the entry point + // For single-file applications, the executable is the entry point + using var currentProcess = Process.GetCurrentProcess(); var filePath = - Process.GetCurrentProcess().MainModule?.FileName - ?? throw new InvalidOperationException("Can't get current process main module."); + currentProcess.MainModule?.FileName + ?? throw new InvalidOperationException( + "Failed to get the current process's entry point." + ); return FromAssembly(assembly, filePath); } diff --git a/Onova/Models/CheckForUpdatesResult.cs b/Onova/Models/CheckForUpdatesResult.cs index dc516e8..ba380d8 100644 --- a/Onova/Models/CheckForUpdatesResult.cs +++ b/Onova/Models/CheckForUpdatesResult.cs @@ -6,35 +6,25 @@ namespace Onova.Models; /// /// Result of checking for updates. /// -public class CheckForUpdatesResult +public class CheckForUpdatesResult( + IReadOnlyList versions, + Version? lastVersion, + bool canUpdate +) { /// /// All available package versions. /// - public IReadOnlyList Versions { get; } + public IReadOnlyList Versions { get; } = versions; /// /// Last available package version. /// Null if there are no available packages. /// - public Version? LastVersion { get; } + public Version? LastVersion { get; } = lastVersion; /// /// Whether there is a package with higher version than the current version. /// - public bool CanUpdate { get; } - - /// - /// Initializes a new instance of . - /// - public CheckForUpdatesResult( - IReadOnlyList versions, - Version? lastVersion, - bool canUpdate - ) - { - Versions = versions; - LastVersion = lastVersion; - CanUpdate = canUpdate; - } + public bool CanUpdate { get; } = canUpdate; } diff --git a/Onova/Onova.csproj b/Onova/Onova.csproj index 166de75..62d12d6 100644 --- a/Onova/Onova.csproj +++ b/Onova/Onova.csproj @@ -1,8 +1,10 @@  - netstandard2.0;netcoreapp3.0;net462 + netstandard2.0;netcoreapp3.0;net462;net8.0 true + true + true @@ -22,12 +24,13 @@ - - + + + - - - + + + diff --git a/Onova/Services/AggregatePackageResolver.cs b/Onova/Services/AggregatePackageResolver.cs index 68b1ef0..333079b 100644 --- a/Onova/Services/AggregatePackageResolver.cs +++ b/Onova/Services/AggregatePackageResolver.cs @@ -11,18 +11,8 @@ namespace Onova.Services; /// /// Resolves packages using multiple other package resolvers. /// -public class AggregatePackageResolver : IPackageResolver +public class AggregatePackageResolver(IReadOnlyList resolvers) : IPackageResolver { - private readonly IReadOnlyList _resolvers; - - /// - /// Initializes an instance of . - /// - public AggregatePackageResolver(IReadOnlyList resolvers) - { - _resolvers = resolvers; - } - /// /// Initializes an instance of . /// @@ -37,7 +27,7 @@ public async Task> GetPackageVersionsAsync( var aggregateVersions = new HashSet(); // Get unique package versions provided by all resolvers - foreach (var resolver in _resolvers) + foreach (var resolver in resolvers) { var versions = await resolver.GetPackageVersionsAsync(cancellationToken); aggregateVersions.AddRange(versions); @@ -52,7 +42,7 @@ CancellationToken cancellationToken ) { // Try to find the first resolver that has this package version - foreach (var resolver in _resolvers) + foreach (var resolver in resolvers) { var versions = await resolver.GetPackageVersionsAsync(cancellationToken); if (versions.Contains(version)) diff --git a/Onova/Services/GithubPackageResolver.cs b/Onova/Services/GithubPackageResolver.cs index 585baef..5efa59d 100644 --- a/Onova/Services/GithubPackageResolver.cs +++ b/Onova/Services/GithubPackageResolver.cs @@ -19,14 +19,14 @@ namespace Onova.Services; /// Resolves packages from release assets of a GitHub repository. /// Release names should contain package versions (e.g. "v1.8.3"). /// -public class GithubPackageResolver : IPackageResolver +public class GithubPackageResolver( + HttpClient http, + string apiBaseAddress, + string repositoryOwner, + string repositoryName, + string assetNamePattern +) : IPackageResolver { - private readonly HttpClient _httpClient; - private readonly string _apiBaseAddress; - private readonly string _repositoryOwner; - private readonly string _repositoryName; - private readonly string _assetNamePattern; - private EntityTagHeaderValue? _cachedPackageVersionUrlMapETag; private IReadOnlyDictionary? _cachedPackageVersionUrlMap; @@ -34,36 +34,13 @@ public class GithubPackageResolver : IPackageResolver /// Initializes an instance of . /// public GithubPackageResolver( - HttpClient httpClient, - string apiBaseAddress, - string repositoryOwner, - string repositoryName, - string assetNamePattern - ) - { - _httpClient = httpClient; - _apiBaseAddress = apiBaseAddress; - _repositoryOwner = repositoryOwner; - _repositoryName = repositoryName; - _assetNamePattern = assetNamePattern; - } - - /// - /// Initializes an instance of . - /// - public GithubPackageResolver( - HttpClient httpClient, + HttpClient http, string repositoryOwner, string repositoryName, string assetNamePattern ) - : this( - httpClient, - "https://api.github.com", - repositoryOwner, - repositoryName, - assetNamePattern - ) { } + : this(http, "https://api.github.com", repositoryOwner, repositoryName, assetNamePattern) + { } /// /// Initializes an instance of . @@ -120,12 +97,14 @@ private IReadOnlyDictionary ParsePackageVersionUrlMap(JsonEleme var assetName = assetJson.GetProperty("name").GetString(); var assetUrl = assetJson.GetProperty("url").GetString(); - // See if name matches if ( string.IsNullOrWhiteSpace(assetName) - || !WildcardPattern.IsMatch(assetName, _assetNamePattern) + || string.IsNullOrWhiteSpace(assetUrl) + || !WildcardPattern.IsMatch(assetName, assetNamePattern) ) + { continue; + } // Add to dictionary map[version] = assetUrl; @@ -140,7 +119,7 @@ CancellationToken cancellationToken ) { // Get releases - var url = $"{_apiBaseAddress}/repos/{_repositoryOwner}/{_repositoryName}/releases"; + var url = $"{apiBaseAddress}/repos/{repositoryOwner}/{repositoryName}/releases"; using var request = new HttpRequestMessage(HttpMethod.Get, url); // Set If-None-Match header if ETag is available @@ -148,7 +127,7 @@ CancellationToken cancellationToken request.Headers.IfNoneMatch.Add(_cachedPackageVersionUrlMapETag); // Get response - using var response = await _httpClient.SendAsync( + using var response = await http.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, cancellationToken @@ -202,7 +181,7 @@ public async Task DownloadPackageAsync( using var request = new HttpRequestMessage(HttpMethod.Get, packageUrl); request.Headers.Add("Accept", "application/octet-stream"); // required - using var response = await _httpClient.SendAsync( + using var response = await http.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, cancellationToken diff --git a/Onova/Services/LocalPackageResolver.cs b/Onova/Services/LocalPackageResolver.cs index a657c92..1fd331f 100644 --- a/Onova/Services/LocalPackageResolver.cs +++ b/Onova/Services/LocalPackageResolver.cs @@ -15,34 +15,23 @@ namespace Onova.Services; /// Resolves packages from a local repository. /// Package file names should contain package versions (e.g. "MyProject-v1.8.3.onv"). /// -public class LocalPackageResolver : IPackageResolver +public class LocalPackageResolver(string repositoryDirPath, string fileNamePattern = "*") + : IPackageResolver { - private readonly string _repositoryDirPath; - private readonly string _fileNamePattern; - - /// - /// Initializes an instance of . - /// - public LocalPackageResolver(string repositoryDirPath, string fileNamePattern = "*") - { - _repositoryDirPath = repositoryDirPath; - _fileNamePattern = fileNamePattern; - } - private IReadOnlyDictionary GetPackageVersionFilePathMap() { var map = new Dictionary(); // Check if repository directory exists - if (!Directory.Exists(_repositoryDirPath)) + if (!Directory.Exists(repositoryDirPath)) return map; // Enumerate files in repository directory - foreach (var filePath in Directory.EnumerateFiles(_repositoryDirPath)) + foreach (var filePath in Directory.EnumerateFiles(repositoryDirPath)) { // See if the name matches var fileName = Path.GetFileName(filePath); - if (!WildcardPattern.IsMatch(fileName, _fileNamePattern)) + if (!WildcardPattern.IsMatch(fileName, fileNamePattern)) continue; // Try to parse version diff --git a/Onova/Services/NugetPackageExtractor.cs b/Onova/Services/NugetPackageExtractor.cs index 4b6154c..73098da 100644 --- a/Onova/Services/NugetPackageExtractor.cs +++ b/Onova/Services/NugetPackageExtractor.cs @@ -12,18 +12,8 @@ namespace Onova.Services; /// /// Extracts files from NuGet packages. /// -public class NugetPackageExtractor : IPackageExtractor +public class NugetPackageExtractor(string rootDirPath) : IPackageExtractor { - private readonly string _rootDirPath; - - /// - /// Initializes an instance of . - /// - public NugetPackageExtractor(string rootDirPath) - { - _rootDirPath = rootDirPath; - } - /// public async Task ExtractPackageAsync( string sourceFilePath, @@ -37,8 +27,9 @@ public async Task ExtractPackageAsync( // Get entries in the content directory var entries = archive - .Entries - .Where(e => e.FullName.StartsWith(_rootDirPath, StringComparison.OrdinalIgnoreCase)) + .Entries.Where(e => + e.FullName.StartsWith(rootDirPath, StringComparison.OrdinalIgnoreCase) + ) .ToArray(); // For progress reporting @@ -49,7 +40,7 @@ public async Task ExtractPackageAsync( foreach (var entry in entries) { // Get relative entry path - var relativeEntryPath = entry.FullName[_rootDirPath.Length..].TrimStart('/', '\\'); + var relativeEntryPath = entry.FullName[rootDirPath.Length..].TrimStart('/', '\\'); // Get destination paths var entryDestFilePath = Path.Combine(destDirPath, relativeEntryPath); @@ -64,7 +55,9 @@ public async Task ExtractPackageAsync( entry.FullName.Last() == Path.DirectorySeparatorChar || entry.FullName.Last() == Path.AltDirectorySeparatorChar ) + { continue; + } // Extract entry using var input = entry.Open(); diff --git a/Onova/Services/NugetPackageResolver.cs b/Onova/Services/NugetPackageResolver.cs index 5f5dfb1..7b88131 100644 --- a/Onova/Services/NugetPackageResolver.cs +++ b/Onova/Services/NugetPackageResolver.cs @@ -15,36 +15,23 @@ namespace Onova.Services; /// /// Resolves packages from a NuGet feed. /// -public class NugetPackageResolver : IPackageResolver +public class NugetPackageResolver(HttpClient http, string serviceIndexUrl, string packageId) + : IPackageResolver { - private readonly HttpClient _httpClient; - private readonly string _serviceIndexUrl; - private readonly string _packageId; - - private string PackageIdNormalized => _packageId.ToLowerInvariant(); - - /// - /// Initializes an instance of . - /// - public NugetPackageResolver(HttpClient httpClient, string serviceIndexUrl, string packageId) - { - _httpClient = httpClient; - _serviceIndexUrl = serviceIndexUrl; - _packageId = packageId; - } - /// /// Initializes an instance of . /// public NugetPackageResolver(string serviceIndexUrl, string packageId) : this(Http.Client, serviceIndexUrl, packageId) { } + private string PackageIdNormalized { get; } = packageId.ToLowerInvariant(); + private async Task GetPackageBaseAddressResourceUrlAsync( CancellationToken cancellationToken ) { // Get all available resources - var responseJson = await _httpClient.GetJsonAsync(_serviceIndexUrl, cancellationToken); + var responseJson = await http.GetJsonAsync(serviceIndexUrl, cancellationToken); var resourcesJson = responseJson.GetProperty("resources"); // Get URL of the required resource @@ -59,7 +46,11 @@ CancellationToken cancellationToken StringComparison.OrdinalIgnoreCase ) ) - return resourceJson.GetProperty("@id").GetString(); + { + var url = resourceJson.GetProperty("@id").GetString(); + if (!string.IsNullOrWhiteSpace(url)) + return url; + } } // Resource not found @@ -76,7 +67,7 @@ public async Task> GetPackageVersionsAsync( // Get versions var request = $"{resourceUrl}/{PackageIdNormalized}/index.json"; - var responseJson = await _httpClient.GetJsonAsync(request, cancellationToken); + var responseJson = await http.GetJsonAsync(request, cancellationToken); var versionsJson = responseJson.GetProperty("versions"); var versions = new HashSet(); @@ -107,7 +98,7 @@ public async Task DownloadPackageAsync( $"{resourceUrl}/{PackageIdNormalized}/{version}/{PackageIdNormalized}.{version}.nupkg"; // Get response - using var response = await _httpClient.GetAsync( + using var response = await http.GetAsync( packageUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken diff --git a/Onova/Services/WebPackageResolver.cs b/Onova/Services/WebPackageResolver.cs index b224449..3872338 100644 --- a/Onova/Services/WebPackageResolver.cs +++ b/Onova/Services/WebPackageResolver.cs @@ -15,20 +15,8 @@ namespace Onova.Services; /// Resolves packages using a manifest served by a web server. /// Manifest consists of package versions and URLs, separated by space, one line per version. /// -public class WebPackageResolver : IPackageResolver +public class WebPackageResolver(HttpClient http, string manifestUrl) : IPackageResolver { - private readonly HttpClient _httpClient; - private readonly string _manifestUrl; - - /// - /// Initializes an instance of . - /// - public WebPackageResolver(HttpClient httpClient, string manifestUrl) - { - _httpClient = httpClient; - _manifestUrl = manifestUrl; - } - /// /// Initializes an instance of . /// @@ -37,7 +25,7 @@ public WebPackageResolver(string manifestUrl) private string ExpandRelativeUrl(string url) { - var manifestUri = new Uri(_manifestUrl); + var manifestUri = new Uri(manifestUrl); var uri = new Uri(manifestUri, url); return uri.ToString(); @@ -50,7 +38,7 @@ CancellationToken cancellationToken var map = new Dictionary(); // Get manifest - var response = await _httpClient.GetStringAsync(_manifestUrl, cancellationToken); + var response = await http.GetStringAsync(manifestUrl, cancellationToken); foreach (var line in response.Split('\n')) { @@ -102,7 +90,7 @@ public async Task DownloadPackageAsync( throw new PackageNotFoundException(version); // Download - using var response = await _httpClient.GetAsync( + using var response = await http.GetAsync( packageUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken diff --git a/Onova/UpdateManager.cs b/Onova/UpdateManager.cs index c7ae155..87d9fd8 100644 --- a/Onova/UpdateManager.cs +++ b/Onova/UpdateManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using Onova.Exceptions; @@ -18,6 +19,7 @@ namespace Onova; /// /// Entry point for handling application updates. /// +[SupportedOSPlatform("windows")] public class UpdateManager : IUpdateManager { private const string UpdaterResourceName = "Onova.Updater.exe";