diff --git a/src/Core/BellsAndWhistles.cs b/src/Core/BellsAndWhistles.cs index 7c6fde4..d168b07 100644 --- a/src/Core/BellsAndWhistles.cs +++ b/src/Core/BellsAndWhistles.cs @@ -11,13 +11,13 @@ internal class BellsAndWhistles { private static readonly string assemblyEnvironment = string.Format("[{1}, {0}]", RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(), RuntimeInformation.FrameworkDescription); - private static readonly Assembly entryAssembly = Assembly.GetEntryAssembly(); + private static readonly Assembly entryAssembly = Assembly.GetEntryAssembly()!; - private static readonly string assemblyVersion = entryAssembly.GetName().Version.ToString(); + private static readonly string assemblyVersion = entryAssembly.GetName().Version!.ToString(); - private static readonly string fileVersion = entryAssembly.GetCustomAttribute().Version; + private static readonly string fileVersion = entryAssembly.GetCustomAttribute()!.Version; - private static readonly string assemblyName = entryAssembly.GetCustomAttribute().Product; + private static readonly string assemblyName = entryAssembly.GetCustomAttribute()!.Product; const string assemblyBuildType = #if DEBUG "[Debug]" @@ -31,7 +31,12 @@ internal class BellsAndWhistles public static void ShowVersionInfo(ILogger logger) { - logger.LogInformation($"{assemblyName} {assemblyVersion} {assemblyEnvironment} {assemblyBuildType}"); + logger.LogInformation( + "{assemblyName} {assemblyVersion} {assemblyEnvironment} {assemblyBuildType}", + assemblyName, + assemblyVersion, + assemblyEnvironment, + assemblyBuildType); Console.BackgroundColor = ConsoleColor.Black; WriteColorLine("%╔═╦═══════════════════════════════════════════════════════════════════════════════════════╦═╗"); WriteColorLine("%╠═╝ .''. %╚═%╣"); diff --git a/src/Core/DownloaderService.cs b/src/Core/DownloaderService.cs index 42cf0f9..81b9bf5 100644 --- a/src/Core/DownloaderService.cs +++ b/src/Core/DownloaderService.cs @@ -16,12 +16,15 @@ using System.Threading.Tasks; internal class DownloaderService : IHostedService { + private static readonly JsonSerializerOptions jsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; private readonly ILogger logger; private readonly DownloaderOptions options; private readonly HttpClient client; private readonly IHostApplicationLifetime hostApplicationLifetime; - public DownloaderService(IHostApplicationLifetime hostApplicationLifetime, ILogger logger, HttpClient client, DownloaderOptions options) { this.logger = logger; @@ -87,15 +90,12 @@ internal class DownloaderService : IHostedService this.hostApplicationLifetime.StopApplication(); } - private async Task<(string json, IDictionary versions)> GetJson(string feedUrl, string productVersion = null, CancellationToken cancellationToken = default) + private async Task<(string json, IDictionary versions)> GetJson(string feedUrl, string? productVersion = null, CancellationToken cancellationToken = default) { var atlassianJson = await this.client.GetStringAsync(feedUrl, cancellationToken).ConfigureAwait(false); var json = atlassianJson.Trim()["downloads(".Length..^1]; - this.logger.LogTrace($"Downloaded json: {0}", json); - var parsed = JsonSerializer.Deserialize(json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); + this.logger.LogTrace("Downloaded json: {json}", json); + var parsed = JsonSerializer.Deserialize(json, jsonOptions)!; this.logger.LogDebug("Found {releaseCount} releases", parsed.Length); var versions = parsed .GroupBy(a => a.Version) @@ -112,7 +112,6 @@ internal class DownloaderService : IHostedService private async Task DownloadFilesFromFeed(string feedUrl, IDictionary versions, CancellationToken cancellationToken) { - var feedDir = Path.Combine(this.options.OutputDir, feedUrl[(feedUrl.LastIndexOf('/') + 1)..feedUrl.LastIndexOf('.')]); this.logger.LogInformation("Download from JSON \"{feedUrl}\" started", feedUrl); foreach (var version in versions) @@ -137,12 +136,10 @@ internal class DownloaderService : IHostedService if (file.ZipUrl == null) { - this.logger.LogWarning($"Empty ZipUrl found for version '{version.Key}' in {feedUrl}"); + this.logger.LogWarning("Empty ZipUrl found for version '{version}' in {feedUrl}", version.Key, feedUrl); continue; } - - var serverPath = file.ZipUrl.PathAndQuery; var outputFile = Path.Combine(directory, serverPath[(serverPath.LastIndexOf('/') + 1)..]); if (!File.Exists(outputFile)) @@ -153,58 +150,64 @@ internal class DownloaderService : IHostedService { if (this.options.SkipFileCheck == false) { - this.logger.LogWarning($"File \"{outputFile}\" already exists. File sizes will be compared."); + this.logger.LogWarning("File \"{outputFile}\" already exists. File sizes will be compared.", outputFile); var localFileSize = new FileInfo(outputFile).Length; - this.logger.LogInformation($"Size of local file is {localFileSize} bytes."); + this.logger.LogInformation("Size of local file is {localFileSize} bytes.", localFileSize); try { var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("User-Agent", options.UserAgent); - var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, file.ZipUrl)); + var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, file.ZipUrl), cancellationToken); if (response.IsSuccessStatusCode) { if (response.Content.Headers.ContentLength.HasValue) { var remoteFileSize = response.Content.Headers.ContentLength.Value; - this.logger.LogInformation($"Size of remote file is \"{remoteFileSize}\" bytes."); + this.logger.LogInformation("Size of remote file is \"{remoteFileSize}\" bytes.", remoteFileSize); if (remoteFileSize == localFileSize) { - this.logger.LogInformation($"Size of remote and local files and are same ({remoteFileSize} bytes and {localFileSize} bytes). Nothing to download. Operation skipped."); + this.logger.LogInformation( + "Size of remote and local files and are same ({remoteFileSize} bytes and {localFileSize} bytes). Nothing to download. Operation skipped.", + remoteFileSize, + localFileSize); } else { - this.logger.LogWarning($"Size of remote and local files and are not same ({remoteFileSize} bytes and {localFileSize} bytes). Download started."); + this.logger.LogWarning( + "Size of remote and local files and are not same ({remoteFileSize} bytes and {localFileSize} bytes). Download started.", + remoteFileSize, + localFileSize); File.Delete(outputFile); await this.DownloadFile(file, outputFile, cancellationToken).ConfigureAwait(false); } } else { - this.logger.LogWarning($"Cant get size of remote file \"{file.ZipUrl}\". May be server not support it feature. Sorry."); + this.logger.LogWarning("Cant get size of remote file \"{uri}\". May be server not support it feature. Sorry.", file.ZipUrl); continue; } } else { - this.logger.LogCritical($"Request execution error: \"{response.StatusCode}\". Sorry."); + this.logger.LogError("Request execution error for {uri}: \"{statusCode}\". Sorry.", file.ZipUrl, response.StatusCode); } } catch (HttpRequestException ex) { - this.logger.LogCritical($"HTTP request error: \"{ex.Message}\", \"{ex.StackTrace}\", \"{ex.StatusCode}\". Sorry."); - } + this.logger.LogError(ex, "HTTP request error for {uri}: \"{message}\", \"{statusCode}\". Sorry.", file.ZipUrl, ex.Message, ex.StatusCode); } + } else { - logger.LogWarning($"File \"{outputFile}\" already exists. Download from \"{file.ZipUrl}\" skipped."); + logger.LogWarning("File \"{outputFile}\" already exists. Download from \"{uri}\" skipped.", outputFile, file.ZipUrl); continue; } } } } - this.logger.LogInformation($"All files from \"{feedUrl}\" successfully downloaded."); + this.logger.LogInformation("All files from \"{feedUrl}\" successfully downloaded.", feedUrl); } @@ -230,14 +233,13 @@ internal class DownloaderService : IHostedService } catch (Exception removeEx) { - this.logger.LogError(removeEx, $"Failed to remove incomplete file \"{outputFile}\"."); + this.logger.LogError(removeEx, "Failed to remove incomplete file \"{outputFile}\".", outputFile); } } - this.logger.LogInformation($"File \"{file.ZipUrl}\" successfully downloaded to \"{outputFile}\"."); + this.logger.LogInformation("File \"{uri}\" successfully downloaded to \"{outputFile}\".", file.ZipUrl, outputFile); } - #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public async Task StopAsync(CancellationToken cancellationToken) { } #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously diff --git a/src/Models/DownloaderOptions.cs b/src/Models/DownloaderOptions.cs index 46c36b3..289cd54 100644 --- a/src/Models/DownloaderOptions.cs +++ b/src/Models/DownloaderOptions.cs @@ -1,4 +1,11 @@ namespace EpicMorg.Atlassian.Downloader; using System; -public record DownloaderOptions(string OutputDir, Uri[] CustomFeed, DownloadAction Action, bool Version, string ProductVersion, bool SkipFileCheck, string UserAgent) { } \ No newline at end of file +public record DownloaderOptions( + string OutputDir, + Uri[]? CustomFeed, + DownloadAction Action, + bool Version, + string? ProductVersion, + bool SkipFileCheck, + string UserAgent) { } \ No newline at end of file diff --git a/src/Models/ResponseItem.cs b/src/Models/ResponseItem.cs index 3e0fff1..3702463 100644 --- a/src/Models/ResponseItem.cs +++ b/src/Models/ResponseItem.cs @@ -4,16 +4,16 @@ using System; public partial class ResponseItem { - public string Description { get; set; } - public string Edition { get; set; } - public Uri ZipUrl { get; set; } - public object TarUrl { get; set; } - public string Md5 { get; set; } - public string Size { get; set; } - public string Released { get; set; } - public string Type { get; set; } - public string Platform { get; set; } - public string Version { get; set; } - public Uri ReleaseNotes { get; set; } - public Uri UpgradeNotes { get; set; } + public string? Description { get; set; } + public string? Edition { get; set; } + public Uri? ZipUrl { get; set; } + public object? TarUrl { get; set; } + public string? Md5 { get; set; } + public string? Size { get; set; } + public string? Released { get; set; } + public string? Type { get; set; } + public string? Platform { get; set; } + public required string Version { get; set; } + public Uri? ReleaseNotes { get; set; } + public Uri? UpgradeNotes { get; set; } } \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index 1746dd4..eacb8a3 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -24,7 +24,14 @@ public class Program /// Override target version to download some product. Advice: Use it with "customFeed". /// Skip compare of file sizes if a local file already exists. Existing file will be skipped to check and redownload. /// Set custom user agent via this feature flag. - static async Task Main(string outputDir, Uri[] customFeed = null, DownloadAction action = DownloadAction.Download, bool about = false, string productVersion = null, bool skipFileCheck = false, string userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0") => await + static async Task Main( + string? outputDir = default, + Uri[]? customFeed = null, + DownloadAction action = DownloadAction.Download, + bool about = false, + string? productVersion = null, + bool skipFileCheck = false, + string userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0") => await Host .CreateDefaultBuilder() .ConfigureHostConfiguration(configHost => configHost.AddEnvironmentVariables()) @@ -46,7 +53,14 @@ public class Program .AddSerilog(dispose: true); }) .AddHostedService() - .AddSingleton(new DownloaderOptions(outputDir, customFeed, action, about, productVersion, skipFileCheck, userAgent)) + .AddSingleton(new DownloaderOptions( + outputDir ?? Environment.CurrentDirectory, + customFeed, + action, + about, + productVersion, + skipFileCheck, + userAgent)) .AddHttpClient()) .RunConsoleAsync() .ConfigureAwait(false); diff --git a/src/atlassian-downloader.csproj b/src/atlassian-downloader.csproj index 5f4e8ea..493a125 100644 --- a/src/atlassian-downloader.csproj +++ b/src/atlassian-downloader.csproj @@ -10,6 +10,7 @@ win-x64 + enable Exe net8.0 false