diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c2e909e..9e1b1aa 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -2,7 +2,7 @@ patreon: epicmorg ko_fi: epicmorg -github: epicmorg, kasthack, stamepicmorg # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] #open_collective: # Replace with a single Open Collective username #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 0000000..c379cef --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,20 @@ +# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome + +# Comment to be posted to on first time issues +newIssueWelcomeComment: > + :wave: Thank you for opening your first issue. I'm just an automated bot that's here to help you get the information you need quicker, so please ignore this message if it doesn't apply to your issue. + +# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome + +# Comment to be posted to on PRs from first time contributors in your repository +newPRWelcomeComment: > + Congratulations on opening your first Pull Request, this is a momentous day for you and us! :sparkles: + + +# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge + +# Comment to be posted to on pull requests merged by a first time user +firstPRMergeComment: > + Hooray! Your first Pull Request was merged, here's to many more :rocket: + +# It is recommend to include as many gifs and emojis as possible diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 0000000..7e40c03 --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,13 @@ +# Configuration for probot-no-response - https://github.com/probot/no-response + +# Number of days of inactivity before an Issue is closed for lack of response +daysUntilClose: 7 +# Label requiring a response +responseRequiredLabel: more-information-needed +# Comment to post when closing an Issue for lack of response. Set to `false` to disable +closeComment: > + This issue has been automatically closed because there has been no response + to our request for more information from the original author. With only the + information that is currently in the issue, we don't have enough information + to take action. Please reach out if you have or find the answers we need so + that we can investigate further. diff --git a/.github/screenshot-1.png b/.github/screenshot-1.png index 9bba4bb..8ffc6dd 100644 Binary files a/.github/screenshot-1.png and b/.github/screenshot-1.png differ diff --git a/.github/screenshot-2.png b/.github/screenshot-2.png new file mode 100644 index 0000000..a418060 Binary files /dev/null and b/.github/screenshot-2.png differ diff --git a/.github/screenshot-3.png b/.github/screenshot-3.png new file mode 100644 index 0000000..f96e2fc Binary files /dev/null and b/.github/screenshot-3.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 16bdf32..d0247bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Atlassian Downloader - Changelog -* `1.0.0.6` - added support of `clover`. fixed broken json parsing. added new logger. +* `1.0.0.7` - added `unofficial support` of `sourcetree` via automatic mirror [from github](https://github.com/EpicMorg/atlassian-json). fixed `logger` output, code improvments. +* `1.0.0.6` - added support of `clover`. fixed broken json parsing. added new `logger`. * `1.0.0.5` - added support for `EAP` releases. * `1.0.0.4` - bump version. rewrited build scripts. added support of `arm` and `arm64`. * `1.0.0.3` - some cosmetics improvements. diff --git a/README.md b/README.md index 3984be1..939c4a9 100644 --- a/README.md +++ b/README.md @@ -7,30 +7,67 @@ Console app written with `c#` and `dotnet5` for downloading all avalible product ![Atlassian Downloader](https://rawcdn.githack.com/EpicMorg/atlassian-downloader/8fd59dfb0514aeff8556761c2f9862185d3489ea/.github/screenshot-1.png) ## Requerments -1. Preinstalled `dotnet5`. Download [here](https://dotnet.microsoft.com/download/dotnet/5.0). +1. Preinstalled`*` `dotnet5`. Download [here](https://dotnet.microsoft.com/download/dotnet/5.0). 2. Supported OS: `win32/win64`, `linux`, `macosx`, `arm/arm64` -## How to +`*` since version `1.0.0.4` application build asstandalone package and do not requre preinstalled `dotnet5`. + +# How to... +## ..bootstrap from scratch 1. `git clone` this repo. 2. `cd` to `/src`. -3. execute `donten run` in `src` folder. +3.1 execute `donten run` in `src` folder. +or +3.2 execute `build.bat(sh)` in `src` folder. 4. by default all data will be downloaded to `src/atlassian` folder and subfolders. -## Usage +## ..install +1. download latest [![Release](https://img.shields.io/github/v/release/EpicMorg/atlassian-downloader?style=flat-square)](https://github.com/EpicMorg/atlassian-downloader/releases) +2. ... +3. profit! + +# Usage and settings +## CLI args ``` +atlassian-downloader: + Atlassian archive downloader. See https://github.com/EpicMorg/atlassian-downloader for more info + Usage: atlassian-downloader [options] Options: - --output-dir Override output directory to download [default: atlassian] - --list Show all download links from feed without downloading [default: False] - --custom-feed Override URIs to import. [default: ] - --version Show version information - -?, -h, --help Show help and usage information + --output-dir Override output directory to download [default: atlassian] + --custom-feed Override URIs to import [default: ] + --action Action to perform [default: Download] + --version Show credits banner [default: False] + -?, -h, --help Show help and usage information ``` +## Additional settings +File `src/appSettings.json` contains additional settings, like [loglevel](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=dotnet-plat-ext-5.0#fields) and [console output theme](https://github.com/serilog/serilog-sinks-console). You can set it up via editing this file. + +### Supported log levels +| Level | Enum | Description +|-------------|:-------------:|-------------| +| `Critical` | `5` | Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires immediate attention. +| `Debug` | `1` | Logs that are used for interactive investigation during development. These logs should primarily contain information useful for debugging and have no long-term value. +| `Error` | `4` | Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a failure in the current activity, not an application-wide failure. +| `Information` | `2` | Logs that track the general flow of the application. These logs should have long-term value. +| `None` | `6` | Not used for writing log messages. Specifies that a logging category should not write any messages. +| `Trace` | `0` | Logs that contain the most detailed messages. These messages may contain sensitive application data. These messages are disabled by default and should never be enabled in a production environment. +| `Warning` | `3` | Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application execution to stop. + +### Supported console themes +The following built-in themes are available, provided by `Serilog.Sinks.Console` package: + + * `ConsoleTheme.None` - no styling + * `SystemConsoleTheme.Literate` - styled to replicate _Serilog.Sinks.Literate_, using the `System.Console` coloring modes supported on all Windows/.NET targets; **this is the default when no theme is specified** + * `SystemConsoleTheme.Grayscale` - a theme using only shades of gray, white, and black + * `AnsiConsoleTheme.Literate` - an ANSI 16-color version of the "literate" theme; we expect to update this to use 256-colors for a more refined look in future + * `AnsiConsoleTheme.Grayscale` - an ANSI 256-color version of the "grayscale" theme + * `AnsiConsoleTheme.Code` - an ANSI 256-color Visual Studio Code-inspired theme -## Supported products: +# Supported products: | Product | Current | Archive | EAP | |-------------|:-------------:|:-------------:|:-------------:| @@ -44,7 +81,7 @@ Options: | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Jira%20Core&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/jira/core) | :white_check_mark: | :white_check_mark: | :x: | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Jira%20Software&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/jira) | :white_check_mark: | :white_check_mark: | :white_check_mark: | | [![Product](https://img.shields.io/static/v1?label=Atlassian&message=Jira%20Servicedesk&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/jira/service-management) | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| [![Product](https://img.shields.io/static/v1?label=Atlassian&message=SourceTree&color=yellow&style=for-the-badge)](https://www.atlassian.com/software/sourcetree) | :x: | :x: | :x: | +| [![Product](https://img.shields.io/static/v1?label=Atlassian&message=SourceTree&color=bright%20green&style=for-the-badge)](https://www.atlassian.com/software/sourcetree) | :white_check_mark: | :white_check_mark: | :x: | * Archive of `Atlassian` jsons available [here](https://github.com/EpicMorg/atlassian-json). diff --git a/src/DonloaderService.cs b/src/DonloaderService.cs new file mode 100644 index 0000000..5166b3b --- /dev/null +++ b/src/DonloaderService.cs @@ -0,0 +1,314 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace EpicMorg.Atlassian.Downloader +{ + class DonloaderService : IHostedService + { + private readonly ILogger logger; + private readonly DownloaderOptions options; + private readonly HttpClient client; + private readonly IHostApplicationLifetime hostApplicationLifetime; + private readonly string assemblyEnvironment = string.Format("[{1}, {0}]", + System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(), + System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); + private readonly string assemblyVersion = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion; + + private readonly string assemblyName = Assembly.GetEntryAssembly().GetCustomAttribute().Product; + const string assemblyBuildType = +#if DEBUG + "[Debug]" +#else + + "[Release]" +#endif + ; + + public DonloaderService(IHostApplicationLifetime hostApplicationLifetime, ILogger logger, HttpClient client, DownloaderOptions options) + { + this.logger = logger; + this.client = client; + this.options = options; + this.hostApplicationLifetime = hostApplicationLifetime; + } + public const ConsoleColor DEFAULT = ConsoleColor.Blue; + + public static void WriteColorLine(string text, params object[] args) + { + Dictionary colors = new() + { + { '!', ConsoleColor.Red }, + { '@', ConsoleColor.Green }, + { '#', ConsoleColor.Blue }, + { '$', ConsoleColor.Magenta }, + { '&', ConsoleColor.Yellow }, + { '%', ConsoleColor.Cyan } + }; + // TODO: word wrap, backslash escapes + text = string.Format(text, args); + string chunk = ""; + bool paren = false; + for (int i = 0; i < text.Length; i++) + { + char c = text[i]; + if (colors.ContainsKey(c) && StringNext(text, i) != ' ') + { + Console.Write(chunk); + chunk = ""; + if (StringNext(text, i) == '(') + { + i++; // skip past the paren + paren = true; + } + Console.ForegroundColor = colors[c]; + } + else if (paren && c == ')') + { + paren = false; + Console.ForegroundColor = DEFAULT; + } + else if (Console.ForegroundColor != DEFAULT) + { + Console.Write(c); + if (c == ' ' && !paren) + Console.ForegroundColor = DEFAULT; + } + else + chunk += c; + } + Console.WriteLine(chunk); + Console.ForegroundColor = DEFAULT; + } + + public static char StringPrev(string text, int index) + { + return (index == 0 || text.Length == 0) ? '\0' : text[index - 1]; + } + + public static char StringNext(string text, int index) + { + return (index < text.Length) ? text[index + 1] : '\0'; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + SetConsoleTitle(); + if (options.Version) + { + logger.LogInformation($"{assemblyName} {assemblyVersion} {assemblyEnvironment} {assemblyBuildType}"); + Console.BackgroundColor = ConsoleColor.Black; + WriteColorLine("%╔═╦═══════════════════════════════════════════════════════════════════════════════════════╦═╗"); + WriteColorLine("%╠═╝ .''. %╚═%╣"); + WriteColorLine("%║ .:cc;. %║"); + WriteColorLine("%║ .;cccc;. %║"); + WriteColorLine("%║ .;cccccc;. !╔══════════════════════════════════════════════╗ %║"); + WriteColorLine("%║ .:ccccccc;. !║ " + assemblyName + " !║ %║"); + WriteColorLine("%║ 'ccccccccc;. !╠══════════════════════════════════════════════╣ %║"); + WriteColorLine("%║ ,cccccccccc;. !║ &Code: @kastkack !║ %║"); + WriteColorLine("%║ ,ccccccccccc;. !║ &GFX: @stam !║ %║"); + WriteColorLine("%║ .... .:ccccccccccc;. !╠══════════════════════════════════════════════╣ %║"); + WriteColorLine("%║ .',,'..;cccccccccccc;. !║ &Version: " + assemblyVersion + " !║ %║"); + WriteColorLine("%║ .,,,,,'.';cccccccccccc;. !║ &GitHub: $EpicMorg/atlassian-downloader !║ %║"); + WriteColorLine("%║ .,;;;;;,'.':cccccccccccc;. !╚══════════════════════════════════════════════╝ %║"); + WriteColorLine("%║ .;:;;;;;;,...:cccccccccccc;. %║"); + WriteColorLine("%║ .;:::::;;;;'. .;:ccccccccccc;. %║"); + WriteColorLine("%║ .:cc::::::::,. ..:ccccccccccc;. %║"); + WriteColorLine("%║ .:cccccc:::::' .:ccccccccccc;. %║"); + WriteColorLine("%║ .;:::::::::::,. .;:::::::::::,. %║"); + WriteColorLine("%╠═╗ ............ ............ %╔═╣"); + WriteColorLine("%╚═╩═══════════════════════════════════════════════════════════════════════════════════════╩═╝"); + Console.ResetColor(); + } + else + { + var feedUrls = this.GetFeedUrls(); + + logger.LogInformation($"Task started"); + foreach (var feedUrl in feedUrls) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + var (json, versions) = await this.GetJson(feedUrl, cancellationToken).ConfigureAwait(false); + + switch (options.Action) + { + case DownloadAction.ShowRawJson: + Console.Out.WriteLine(json); + break; + case DownloadAction.Download: + await this.DownloadFilesFromFeed(feedUrl, versions, cancellationToken).ConfigureAwait(false); + break; + case DownloadAction.ListURLs: + foreach (var versionProg in versions) + { + foreach (var file in versionProg.Value) + { + Console.Out.WriteLine(file.ZipUrl); + } + } + break; + case DownloadAction.ListVersions: + foreach (var versionProg in versions) + { + foreach (var file in versionProg.Value) + { + Console.Out.WriteLine(file.Version); + } + } + break; + } + } + } + logger.LogInformation($"Complete"); + + this.hostApplicationLifetime.StopApplication(); + } + + private async Task<(string json, IDictionary versions)> GetJson(string feedUrl, CancellationToken cancellationToken) + { + var atlassianJson = await client.GetStringAsync(feedUrl, cancellationToken).ConfigureAwait(false); + var json = atlassianJson.Trim()["downloads(".Length..^1]; + logger.LogTrace("Downloaded json: {0}", json); + var parsed = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + logger.LogDebug("Found {0} releases", parsed.Length); + var versions = parsed.GroupBy(a => a.Version).ToDictionary(a => a.Key, a => a.ToArray()); + logger.LogDebug("Found {0} releases", versions.Count); + return (json, versions); + } + + private string[] GetFeedUrls() => options.CustomFeed != null + ? options.CustomFeed.Select(a => a.ToString()).ToArray() + : new[] { + "https://my.atlassian.com/download/feeds/archived/bamboo.json", + "https://my.atlassian.com/download/feeds/archived/clover.json", + "https://my.atlassian.com/download/feeds/archived/confluence.json", + "https://my.atlassian.com/download/feeds/archived/crowd.json", + "https://my.atlassian.com/download/feeds/archived/crucible.json", + "https://my.atlassian.com/download/feeds/archived/fisheye.json", + "https://my.atlassian.com/download/feeds/archived/jira-core.json", + "https://my.atlassian.com/download/feeds/archived/jira-servicedesk.json", + "https://my.atlassian.com/download/feeds/archived/jira-software.json", + "https://my.atlassian.com/download/feeds/archived/jira.json", + "https://my.atlassian.com/download/feeds/archived/stash.json", + + "https://my.atlassian.com/download/feeds/current/bamboo.json", + "https://my.atlassian.com/download/feeds/current/clover.json", + "https://my.atlassian.com/download/feeds/current/confluence.json", + "https://my.atlassian.com/download/feeds/current/crowd.json", + "https://my.atlassian.com/download/feeds/current/crucible.json", + "https://my.atlassian.com/download/feeds/current/fisheye.json", + "https://my.atlassian.com/download/feeds/current/jira-core.json", + "https://my.atlassian.com/download/feeds/current/jira-servicedesk.json", + "https://my.atlassian.com/download/feeds/current/jira-software.json", + "https://my.atlassian.com/download/feeds/current/stash.json", + + "https://my.atlassian.com/download/feeds/eap/bamboo.json", + "https://my.atlassian.com/download/feeds/eap/confluence.json", + "https://my.atlassian.com/download/feeds/eap/jira.json", + "https://my.atlassian.com/download/feeds/eap/jira-servicedesk.json", + "https://my.atlassian.com/download/feeds/eap/stash.json", + + //https://raw.githubusercontent.com/EpicMorg/atlassian-json/master/json-backups/archived/sourcetree.json + "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/archived/sourcetree.json", + + //https://raw.githubusercontent.com/EpicMorg/atlassian-json/master/json-backups/current/sourcetree.json + "https://raw.githack.com/EpicMorg/atlassian-json/master/json-backups/current/sourcetree.json" + + }; + + private void SetConsoleTitle() + { + Console.Title = $@"{assemblyName} {assemblyVersion} {assemblyEnvironment} - {assemblyBuildType}"; + } + + private async Task DownloadFilesFromFeed(string feedUrl, IDictionary versions, CancellationToken cancellationToken) + { + + var feedDir = Path.Combine(options.OutputDir, feedUrl[(feedUrl.LastIndexOf('/') + 1)..(feedUrl.LastIndexOf('.'))]); + logger.LogInformation($"Download from JSON \"{feedUrl}\" started"); + foreach (var version in versions) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var directory = Path.Combine(feedDir, version.Key); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + foreach (var file in version.Value) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + if (file.ZipUrl == null) + { + logger.LogWarning($"Empty ZipUrl found for version '{version.Key}' in {feedUrl}"); + continue; + } + var serverPath = file.ZipUrl.PathAndQuery; + var outputFile = Path.Combine(directory, serverPath[(serverPath.LastIndexOf("/") + 1)..]); + if (!File.Exists(outputFile)) + { + await DownloadFile(file, outputFile, cancellationToken).ConfigureAwait(false); + } + else + { + logger.LogWarning($"File \"{outputFile}\" already exists. Download from \"{file.ZipUrl}\" skipped."); + } + } + } + logger.LogInformation($"All files from \"{feedUrl}\" successfully downloaded."); + + } + + private async Task DownloadFile(ResponseItem file, string outputFile, CancellationToken cancellationToken) + { + if (!string.IsNullOrEmpty(file.Md5)) + { + File.WriteAllText(outputFile + ".md5", file.Md5); + } + try + { + using var outputStream = File.OpenWrite(outputFile); + using var request = await client.GetStreamAsync(file.ZipUrl, cancellationToken).ConfigureAwait(false); + await request.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); + } + catch (Exception downloadEx) + { + logger.LogError(downloadEx, $"Failed to download file {file.ZipUrl} to {outputFile}."); + try + { + File.Delete(outputFile); + } + catch (Exception removeEx) + { + logger.LogError(removeEx, $"Failed to remove incomplete file {outputFile}."); + } + } + logger.LogInformation($"File \"{file.ZipUrl}\" successfully downloaded to \"{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 + } +} \ No newline at end of file diff --git a/src/Models/DownloadAction.cs b/src/Models/DownloadAction.cs new file mode 100644 index 0000000..97b3c5e --- /dev/null +++ b/src/Models/DownloadAction.cs @@ -0,0 +1,22 @@ +namespace EpicMorg.Atlassian.Downloader +{ + public enum DownloadAction + { + /// + /// Download application files + /// + Download, + /// + /// Print download URLs and exit + /// + ListURLs, + /// + /// Print available application versions and exit + /// + ListVersions, + /// + /// Print feed JSONs to stdout and exit + /// + ShowRawJson, + } +} \ No newline at end of file diff --git a/src/Models/DownloaderOptions.cs b/src/Models/DownloaderOptions.cs new file mode 100644 index 0000000..aca9cb8 --- /dev/null +++ b/src/Models/DownloaderOptions.cs @@ -0,0 +1,6 @@ +using System; + +namespace EpicMorg.Atlassian.Downloader +{ + public record DownloaderOptions(string OutputDir, Uri[] CustomFeed, DownloadAction Action,bool Version) { } +} \ No newline at end of file diff --git a/src/Models/ResponseItem.cs b/src/Models/ResponseItem.cs new file mode 100644 index 0000000..ce4e66e --- /dev/null +++ b/src/Models/ResponseItem.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace EpicMorg.Atlassian.Downloader +{ + + 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; } + } +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index 517d3ea..85994ad 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,180 +1,49 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Serilog; -using System; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace EpicMorg.Atlassian.Downloader { - class Program : IHostedService { - - private readonly ILogger logger; - private readonly Arguments arguments; - - public Program(ILogger logger,Arguments arguments) { - this.logger = logger; - this.arguments = arguments; - } - /// - /// - /// - /// Override output directory to download - /// Show all download links from feed without downloading - /// Override URIs to import. - /// - static async Task Main(string outputDir = "atlassian", bool list = false, Uri[] customFeed = null) => await - Host - .CreateDefaultBuilder() - .ConfigureHostConfiguration(configHost => configHost.AddEnvironmentVariables()) - .ConfigureAppConfiguration((ctx, configuration) => { - configuration - .SetBasePath(Environment.CurrentDirectory) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{ctx.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables(); - }) - .ConfigureServices((ctx, services) => { - - services - .AddOptions() - .AddLogging(builder => { - builder.ClearProviders(); - Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(ctx.Configuration) - .CreateLogger(); - builder.AddSerilog(dispose: true); - }); - services.AddHostedService(); - services.AddSingleton(new Arguments(outputDir, list, customFeed)); - }) - .RunConsoleAsync(); - - public record Arguments(string outputDir = "atlassian", bool list = false, Uri[] customFeed = null); - - public async Task StartAsync(CancellationToken cancellationToken) { - - var appTitle = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name; - var appVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; - var appStartupDate = DateTime.Now; - var appBuildType = "[Release]"; -#if DEBUG - appBuildType = "[Debug]"; -#endif - - var feedUrls = arguments.customFeed != null - ? arguments.customFeed.Select(a => a.ToString()).ToArray() - : new[] { - "https://my.atlassian.com/download/feeds/archived/bamboo.json", - "https://my.atlassian.com/download/feeds/archived/clover.json", - "https://my.atlassian.com/download/feeds/archived/confluence.json", - "https://my.atlassian.com/download/feeds/archived/crowd.json", - "https://my.atlassian.com/download/feeds/archived/crucible.json", - "https://my.atlassian.com/download/feeds/archived/fisheye.json", - "https://my.atlassian.com/download/feeds/archived/jira-core.json", - "https://my.atlassian.com/download/feeds/archived/jira-servicedesk.json", - "https://my.atlassian.com/download/feeds/archived/jira-software.json", - "https://my.atlassian.com/download/feeds/archived/jira.json", - "https://my.atlassian.com/download/feeds/archived/stash.json", - - "https://my.atlassian.com/download/feeds/current/bamboo.json", - "https://my.atlassian.com/download/feeds/current/clover.json", - "https://my.atlassian.com/download/feeds/current/confluence.json", - "https://my.atlassian.com/download/feeds/current/crowd.json", - "https://my.atlassian.com/download/feeds/current/crucible.json", - "https://my.atlassian.com/download/feeds/current/fisheye.json", - "https://my.atlassian.com/download/feeds/current/jira-core.json", - "https://my.atlassian.com/download/feeds/current/jira-servicedesk.json", - "https://my.atlassian.com/download/feeds/current/jira-software.json", - "https://my.atlassian.com/download/feeds/current/stash.json", - - "https://my.atlassian.com/download/feeds/eap/bamboo.json", - "https://my.atlassian.com/download/feeds/eap/confluence.json", - "https://my.atlassian.com/download/feeds/eap/jira.json", - "https://my.atlassian.com/download/feeds/eap/jira-servicedesk.json", - "https://my.atlassian.com/download/feeds/eap/stash.json" - }; - - Console.Title = $"{appTitle} {appVersion} {appBuildType}"; - logger.LogTrace($"Task started at {appStartupDate}."); - - var client = new HttpClient(); - - foreach (var feedUrl in feedUrls) { - var feedDir = Path.Combine(arguments.outputDir, feedUrl[(feedUrl.LastIndexOf('/') + 1)..(feedUrl.LastIndexOf('.'))]); - var atlassianJson = await client.GetStringAsync(feedUrl); - var callString = "downloads("; - var json = atlassianJson[callString.Length..^1]; - var parsed = JsonSerializer.Deserialize(json, new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - var versionsProg = parsed.GroupBy(a => a.Version).ToDictionary(a => a.Key, a => a.ToArray()); - if (arguments.list) { - foreach (var versionProg in versionsProg) { - foreach (var file in versionProg.Value) { - Console.WriteLine(file.ZipUrl); - } - } - } else { - logger.LogInformation($"Download from JSON \"{feedUrl}\" started at {appStartupDate}."); - foreach (var versionProg in versionsProg) { - var directory = Path.Combine(feedDir, versionProg.Key); - if (!Directory.Exists(directory)) { - Directory.CreateDirectory(directory); - } - foreach (var file in versionProg.Value) { - if (file.ZipUrl == null) { continue; } - var serverPath = file.ZipUrl.PathAndQuery; - var outputFile = Path.Combine(directory, serverPath[(serverPath.LastIndexOf("/") + 1)..]); - if (!File.Exists(outputFile)) { - if (!string.IsNullOrEmpty(file.Md5)) { - File.WriteAllText(outputFile + ".md5", file.Md5); - } - using var outputStream = File.OpenWrite(outputFile); - using var request = await client.GetStreamAsync(file.ZipUrl).ConfigureAwait(false); - await request.CopyToAsync(outputStream).ConfigureAwait(false); - //Console.ForegroundColor = ConsoleColor.Green; - logger.LogInformation($"File \"{file.ZipUrl}\" successfully downloaded to \"{outputFile}\"."); - // Console.ResetColor(); - } else { - // Console.ForegroundColor = ConsoleColor.Yellow; - logger.LogWarning($"File \"{outputFile}\" already exists. Download from \"{file.ZipUrl}\" skipped."); - // Console.ResetColor(); - } - } - } - logger.LogTrace($"All files from \"{feedUrl}\" successfully downloaded."); - } - } - - logger.LogTrace($"Download complete at {appStartupDate}."); - - } - - - public Task StopAsync(CancellationToken cancellationToken) { - throw new NotImplementedException(); - } - } - - public partial class ResponseArray { - 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; } - } - +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using Serilog; + +using System; +using System.Threading.Tasks; + +namespace EpicMorg.Atlassian.Downloader +{ + public class Program + { + /// + /// Atlassian archive downloader. See https://github.com/EpicMorg/atlassian-downloader for more info + /// + /// Action to perform + /// Override output directory to download + /// Override URIs to import + /// Show credits banner + static async Task Main(string OutputDir = "atlassian", Uri[] customFeed = null, DownloadAction Action = DownloadAction.Download, bool Version = false) => await + Host + .CreateDefaultBuilder() + .ConfigureHostConfiguration(configHost => configHost.AddEnvironmentVariables()) + .ConfigureAppConfiguration((ctx, configuration) => + configuration + .SetBasePath(Environment.CurrentDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{ctx.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables()) + .ConfigureServices((ctx, services) => services + .AddOptions() + .AddLogging(builder => + { + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(ctx.Configuration) + .CreateLogger(); + builder + .ClearProviders() + .AddSerilog(dispose: true); + }) + .AddHostedService() + .AddSingleton(new DownloaderOptions(OutputDir, customFeed, Action, Version)) + .AddHttpClient()) + .RunConsoleAsync() + .ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json new file mode 100644 index 0000000..1ea6c9f --- /dev/null +++ b/src/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "atlassian-downloader": { + "commandName": "Project", + "commandLineArgs": "--help" + } + } +} \ No newline at end of file diff --git a/src/appSettings.json b/src/appSettings.json index 89e38d3..330d540 100644 --- a/src/appSettings.json +++ b/src/appSettings.json @@ -1,8 +1,14 @@ { "Serilog": { - "MinimumLevel": "Verbose", + "MinimumLevel": "Information", "WriteTo": [ - "Console", + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Literate, Serilog.Sinks.Console" + } + } + , { "Name": "Logger", "Args": { diff --git a/src/atlassian-downloader.csproj b/src/atlassian-downloader.csproj index 9c19004..0c606eb 100644 --- a/src/atlassian-downloader.csproj +++ b/src/atlassian-downloader.csproj @@ -10,22 +10,24 @@ net5.0 favicon.ico EpicMorg.Atlassian.Downloader - Atlassian Downloader - Atlassian Downloader by EpicMorg, code by kasthack + EpicMorg, kasthack, stam + Atlassian Downloader by EpicMorg https://github.com/EpicMorg/atlassian-downloader favicon.png git https://github.com/EpicMorg/atlassian-downloader atlassian, donwloader, epicmorg - 1.0.0.6 - 1.0.0.6 - 1.0.0.6 + 1.0.0.7 + 1.0.0.7 + 1.0.0.7 EpicMorg 2021 Atlassian Downloader true + EpicMorg +